Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ linters:
- bidichk
- bodyclose
- containedctx
- contextcheck
- decorder
- dogsled
- dupl
Expand Down
10 changes: 9 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ include hack/base.mk

TARGET_BIN ?= build/bin/qubesome

PROTO = pkg/inception/proto

GO_TAGS = -tags 'netgo,osusergo,static_build'
LDFLAGS = -ldflags '-extldflags -static -s -w -X \
github.com/qubesome/cli/cmd/cli.version=$(VERSION)'
Expand All @@ -18,7 +20,13 @@ build: ## build qubesome to the path set by TARGET_BIN.
test: ## run golang tests.
go test -race -parallel 10 ./...

verify: verify-lint verify-dirty ## Run verification checks.
verify: generate verify-lint verify-dirty ## Run verification checks.

verify-lint: $(GOLANGCI)
$(GOLANGCI) run

generate: $(PROTOC)
rm $(PROTO)/*.pb.go || true
PATH=$(TOOLS_BIN) $(PROTOC) --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
$(PROTO)/host.proto
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ qubesome clip to-host i3

- `qubesome start`: Start a qubesome environment for a given profile.
- `qubesome run`: Run qubesome workloads.
- `qubesome host-run`: Run commands on the host but display them in a qubesome profile.
- `qubesome clip`: Manage the images within your workloads.
- `qubesome images`: Manage the images within your workloads.
- `qubesome xdg`: Handle xdg-open based via qubesome.
Expand Down
40 changes: 24 additions & 16 deletions cmd/cli/clipboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,17 @@ func clipboardCommand() *cli.Command {
{
Name: "from-host",
Usage: "copies the clipboard contents from the host to a profile",
Description: `Examples:

qubesome clip from-host - Copy clipboard contents from host to the active profile
qubesome clip from-host -type image/png - Copy image from host clipboard to the active profile
qubesome clip from-host -profile <name> - Copy clipboard contents from host to a specific profile
`,
Arguments: []cli.Argument{
&cli.StringArg{
Name: "target_profile",
Min: 1,
UsageText: "Required when multiple profiles are active",
Min: 0,
Max: 1,
Destination: &targetProfile,
},
Expand All @@ -42,11 +49,9 @@ func clipboardCommand() *cli.Command {
clipType,
},
Action: func(ctx context.Context, c *cli.Command) error {
cfg := profileConfigOrDefault(targetProfile)

target, ok := cfg.Profiles[targetProfile]
if !ok {
return fmt.Errorf("no active profile %q found", targetProfile)
target, err := profileOrActive(targetProfile)
if err != nil {
return err
}

opts := []command.Option[clipboard.Options]{
Expand All @@ -55,7 +60,6 @@ func clipboardCommand() *cli.Command {
}

if typ := c.String("type"); typ != "" {
fmt.Println(typ)
opts = append(opts, clipboard.WithContentType(typ))
}

Expand Down Expand Up @@ -87,12 +91,12 @@ func clipboardCommand() *cli.Command {
Action: func(ctx context.Context, c *cli.Command) error {
cfg := profileConfigOrDefault(targetProfile)

source, ok := cfg.Profiles[sourceProfile]
source, ok := cfg.Profile(sourceProfile)
if !ok {
return fmt.Errorf("no active profile %q found", sourceProfile)
}

target, ok := cfg.Profiles[targetProfile]
target, ok := cfg.Profile(targetProfile)
if !ok {
return fmt.Errorf("no active profile %q found", targetProfile)
}
Expand All @@ -103,7 +107,6 @@ func clipboardCommand() *cli.Command {
}

if typ := c.String("type"); typ != "" {
fmt.Println(typ)
opts = append(opts, clipboard.WithContentType(typ))
}

Expand All @@ -115,10 +118,17 @@ func clipboardCommand() *cli.Command {
{
Name: "to-host",
Usage: "copies the clipboard contents from a profile to the host",
Description: `Examples:

qubesome clip to-host - Copy clipboard contents from the active profile to the host
qubesome clip to-host -type image/png - Copy image from the active profile clipboard to the host
qubesome clip to-host -profile <name> - Copy clipboard contents from a specific profile to the host
`,
Arguments: []cli.Argument{
&cli.StringArg{
Name: "source_profile",
Min: 1,
UsageText: "Required when multiple profiles are active",
Min: 0,
Max: 1,
Destination: &sourceProfile,
},
Expand All @@ -127,11 +137,9 @@ func clipboardCommand() *cli.Command {
clipType,
},
Action: func(ctx context.Context, c *cli.Command) error {
cfg := profileConfigOrDefault(sourceProfile)

target, ok := cfg.Profiles[sourceProfile]
if !ok {
return fmt.Errorf("no active profile %q found", sourceProfile)
target, err := profileOrActive(sourceProfile)
if err != nil {
return err
}

opts := []command.Option[clipboard.Options]{
Expand Down
51 changes: 51 additions & 0 deletions cmd/cli/host_run.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package cli

import (
"context"
"fmt"
"os/exec"

"github.com/urfave/cli/v3"
)

func hostRunCommand() *cli.Command {
cmd := &cli.Command{
Name: "host-run",
Aliases: []string{"hr"},
Usage: "Runs a command at the host, but shows it in a given qubesome profile",
Description: `Examples:

qubesome host-run firefox - Run firefox on the host and display it on the active profile
qubesome host-run -profile <profile> firefox - Run firefox on the host and display it on a specific profile
`,
Arguments: []cli.Argument{
&cli.StringArg{
Name: "command",
Min: 1,
Max: 1,
Destination: &commandName,
},
},
Flags: []cli.Flag{
&cli.StringFlag{
Name: "profile",
Usage: "Required when multiple profiles are active",
Destination: &targetProfile,
},
},
Action: func(ctx context.Context, cmd *cli.Command) error {
prof, err := profileOrActive(targetProfile)
if err != nil {
return err
}

c := exec.Command(commandName)
c.Env = append(c.Env, fmt.Sprintf("DISPLAY=:%d", prof.Display))
out, err := c.CombinedOutput()
fmt.Println(out)

return err
},
}
return cmd
}
11 changes: 11 additions & 0 deletions cmd/cli/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package cli

import (
"context"
"errors"
"fmt"

"github.com/qubesome/cli/internal/images"
"github.com/urfave/cli/v3"
Expand All @@ -27,6 +29,15 @@ func imagesCommand() *cli.Command {
},
Action: func(ctx context.Context, cmd *cli.Command) error {
cfg := profileConfigOrDefault(targetProfile)
if cfg == nil {
return errors.New("could not find qubesome config")
}

if targetProfile != "" {
if _, ok := cfg.Profile(targetProfile); !ok {
return fmt.Errorf("could not find profile %q", targetProfile)
}
}

return images.Run(
images.WithConfig(cfg),
Expand Down
87 changes: 79 additions & 8 deletions cmd/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ package cli

import (
"context"
"errors"
"fmt"
"log/slog"
"os"
"path/filepath"
"strings"

"github.com/qubesome/cli/internal/files"
"github.com/qubesome/cli/internal/log"
Expand All @@ -19,6 +23,7 @@ var (
path string
local string
runner string
commandName string
debug bool
)

Expand All @@ -33,9 +38,17 @@ func RootCommand() *cli.Command {
depsCommand(),
versionCommand(),
completionCommand(),
hostRunCommand(),
},
}

cmd.Before = func(ctx context.Context, c *cli.Command) (context.Context, error) {
if strings.EqualFold(os.Getenv("XDG_SESSION_TYPE"), "wayland") {
fmt.Println("\033[33mWARN: Running qubesome in Wayland is experimental. Some features may not work as expected.\033[0m")
}
return ctx, nil
}

cmd.Flags = append(cmd.Flags, &cli.BoolFlag{
Name: "debug",
Value: false,
Expand Down Expand Up @@ -70,18 +83,76 @@ func config(path string) *types.Config {
}

func profileConfigOrDefault(profile string) *types.Config {
path := files.ProfileConfig(profile)
target, err := os.Readlink(path)
if profile != "" {
// Try to load the profile specific config.
path := files.ProfileConfig(profile)
target, err := os.Readlink(path)

var c *types.Config
if err == nil {
c = config(target)
if err == nil {
c := config(target)
slog.Debug("using profile config", "path", path, "config", c)
if c != nil {
return c
}
}
}

if c != nil {
return c
cfgs := activeConfigs()
if len(cfgs) == 1 {
c := config(cfgs[0])
slog.Debug("using active profile config", "path", cfgs[0], "config", c)
if c != nil && len(c.Profiles) > 0 {
return c
}
}

// Try to load user-level qubesome config.
path = files.QubesomeConfig()
return config(path)
c := config(path)
slog.Debug("using user-level config", "path", path, "config", c)
if c != nil && len(c.Profiles) > 0 {
return c
}

return nil
}

func profileOrActive(profile string) (*types.Profile, error) {
if profile != "" {
cfg := profileConfigOrDefault(profile)
prof, ok := cfg.Profile(profile)
if !ok {
return nil, fmt.Errorf("profile %q not active", profile)
}
return prof, nil
}

cfgs := activeConfigs()
if len(cfgs) > 1 {
return nil, errors.New("multiple profiles active: pick one with -profile")
}
if len(cfgs) == 0 {
return nil, errors.New("no active profile found: start one with qubesome start")
}

f := cfgs[0]
name := strings.TrimSuffix(filepath.Base(f), filepath.Ext(f))
return profileOrActive(name)
}

func activeConfigs() []string {
var active []string

root := files.RunUserQubesome()
entries, err := os.ReadDir(root)
if err == nil {
for _, entry := range entries {
fn := entry.Name()
if filepath.Ext(fn) == ".config" {
active = append(active, filepath.Join(root, fn))
}
}
}

return active
}
16 changes: 13 additions & 3 deletions cmd/cli/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ func runCommand() *cli.Command {
cmd := &cli.Command{
Name: "run",
Aliases: []string{"r"},
Usage: "execute workloads",
Description: `Examples:

qubesome run chrome - Run the chrome workload on the active profile
qubesome run -profile <profile> chrome - Run the chrome workload on a specific profile
`,
Arguments: []cli.Argument{
&cli.StringArg{
Name: "workload",
Expand All @@ -29,13 +35,17 @@ func runCommand() *cli.Command {
Destination: &runner,
},
},
Usage: "execute workloads",
Action: func(ctx context.Context, cmd *cli.Command) error {
cfg := profileConfigOrDefault(targetProfile)
prof, err := profileOrActive(targetProfile)
if err != nil {
return err
}

cfg := profileConfigOrDefault(prof.Name)

return qubesome.Run(
qubesome.WithWorkload(workload),
qubesome.WithProfile(targetProfile),
qubesome.WithProfile(prof.Name),
qubesome.WithConfig(cfg),
qubesome.WithRunner(runner),
qubesome.WithExtraArgs(cmd.Args().Slice()),
Expand Down
7 changes: 6 additions & 1 deletion cmd/cli/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ func startCommand() *cli.Command {
cmd := &cli.Command{
Name: "start",
Aliases: []string{"s"},
Usage: "start qubesome profiles",
Description: `Examples:

qubesome start -git https://github.com/qubesome/sample-dotfiles awesome
qubesome start -git https://github.com/qubesome/sample-dotfiles i3
`,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "git",
Expand Down Expand Up @@ -40,7 +46,6 @@ func startCommand() *cli.Command {
Destination: &targetProfile,
},
},
Usage: "start qubesome profiles",
Action: func(ctx context.Context, cmd *cli.Command) error {
cfg := profileConfigOrDefault(targetProfile)

Expand Down
Loading