Skip to content

Commit f3d40b7

Browse files
authored
New host-run command, CLI improvements and use of gRPC for inception (#7)
Key changes: - Add new `host-run` that allows running commands on host to be displayed on a given qubesome profile. - Examples were added to the CLI help for most commands. - Add profile inference to some commands, so that it is more user friendly. So when a single profile is running, users won't need to type them. Example: `qubesome clip from-host`. - Profiles have a connection with the qubesome process at the host, which enables it from triggering workload execution from the profile itself. That communication is now established via gRPC based on a Unix socket.
2 parents 6521347 + cb12899 commit f3d40b7

File tree

24 files changed

+915
-185
lines changed

24 files changed

+915
-185
lines changed

.golangci.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ linters:
66
- bidichk
77
- bodyclose
88
- containedctx
9-
- contextcheck
109
- decorder
1110
- dogsled
1211
- dupl

Makefile

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ include hack/base.mk
22

33
TARGET_BIN ?= build/bin/qubesome
44

5+
PROTO = pkg/inception/proto
6+
57
GO_TAGS = -tags 'netgo,osusergo,static_build'
68
LDFLAGS = -ldflags '-extldflags -static -s -w -X \
79
github.com/qubesome/cli/cmd/cli.version=$(VERSION)'
@@ -18,7 +20,13 @@ build: ## build qubesome to the path set by TARGET_BIN.
1820
test: ## run golang tests.
1921
go test -race -parallel 10 ./...
2022

21-
verify: verify-lint verify-dirty ## Run verification checks.
23+
verify: generate verify-lint verify-dirty ## Run verification checks.
2224

2325
verify-lint: $(GOLANGCI)
2426
$(GOLANGCI) run
27+
28+
generate: $(PROTOC)
29+
rm $(PROTO)/*.pb.go || true
30+
PATH=$(TOOLS_BIN) $(PROTOC) --go_out=. --go_opt=paths=source_relative \
31+
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
32+
$(PROTO)/host.proto

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ qubesome clip to-host i3
6767

6868
- `qubesome start`: Start a qubesome environment for a given profile.
6969
- `qubesome run`: Run qubesome workloads.
70+
- `qubesome host-run`: Run commands on the host but display them in a qubesome profile.
7071
- `qubesome clip`: Manage the images within your workloads.
7172
- `qubesome images`: Manage the images within your workloads.
7273
- `qubesome xdg`: Handle xdg-open based via qubesome.

cmd/cli/clipboard.go

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,17 @@ func clipboardCommand() *cli.Command {
3030
{
3131
Name: "from-host",
3232
Usage: "copies the clipboard contents from the host to a profile",
33+
Description: `Examples:
34+
35+
qubesome clip from-host - Copy clipboard contents from host to the active profile
36+
qubesome clip from-host -type image/png - Copy image from host clipboard to the active profile
37+
qubesome clip from-host -profile <name> - Copy clipboard contents from host to a specific profile
38+
`,
3339
Arguments: []cli.Argument{
3440
&cli.StringArg{
3541
Name: "target_profile",
36-
Min: 1,
42+
UsageText: "Required when multiple profiles are active",
43+
Min: 0,
3744
Max: 1,
3845
Destination: &targetProfile,
3946
},
@@ -42,11 +49,9 @@ func clipboardCommand() *cli.Command {
4249
clipType,
4350
},
4451
Action: func(ctx context.Context, c *cli.Command) error {
45-
cfg := profileConfigOrDefault(targetProfile)
46-
47-
target, ok := cfg.Profiles[targetProfile]
48-
if !ok {
49-
return fmt.Errorf("no active profile %q found", targetProfile)
52+
target, err := profileOrActive(targetProfile)
53+
if err != nil {
54+
return err
5055
}
5156

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

5762
if typ := c.String("type"); typ != "" {
58-
fmt.Println(typ)
5963
opts = append(opts, clipboard.WithContentType(typ))
6064
}
6165

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

90-
source, ok := cfg.Profiles[sourceProfile]
94+
source, ok := cfg.Profile(sourceProfile)
9195
if !ok {
9296
return fmt.Errorf("no active profile %q found", sourceProfile)
9397
}
9498

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

105109
if typ := c.String("type"); typ != "" {
106-
fmt.Println(typ)
107110
opts = append(opts, clipboard.WithContentType(typ))
108111
}
109112

@@ -115,10 +118,17 @@ func clipboardCommand() *cli.Command {
115118
{
116119
Name: "to-host",
117120
Usage: "copies the clipboard contents from a profile to the host",
121+
Description: `Examples:
122+
123+
qubesome clip to-host - Copy clipboard contents from the active profile to the host
124+
qubesome clip to-host -type image/png - Copy image from the active profile clipboard to the host
125+
qubesome clip to-host -profile <name> - Copy clipboard contents from a specific profile to the host
126+
`,
118127
Arguments: []cli.Argument{
119128
&cli.StringArg{
120129
Name: "source_profile",
121-
Min: 1,
130+
UsageText: "Required when multiple profiles are active",
131+
Min: 0,
122132
Max: 1,
123133
Destination: &sourceProfile,
124134
},
@@ -127,11 +137,9 @@ func clipboardCommand() *cli.Command {
127137
clipType,
128138
},
129139
Action: func(ctx context.Context, c *cli.Command) error {
130-
cfg := profileConfigOrDefault(sourceProfile)
131-
132-
target, ok := cfg.Profiles[sourceProfile]
133-
if !ok {
134-
return fmt.Errorf("no active profile %q found", sourceProfile)
140+
target, err := profileOrActive(sourceProfile)
141+
if err != nil {
142+
return err
135143
}
136144

137145
opts := []command.Option[clipboard.Options]{

cmd/cli/host_run.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package cli
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os/exec"
7+
8+
"github.com/urfave/cli/v3"
9+
)
10+
11+
func hostRunCommand() *cli.Command {
12+
cmd := &cli.Command{
13+
Name: "host-run",
14+
Aliases: []string{"hr"},
15+
Usage: "Runs a command at the host, but shows it in a given qubesome profile",
16+
Description: `Examples:
17+
18+
qubesome host-run firefox - Run firefox on the host and display it on the active profile
19+
qubesome host-run -profile <profile> firefox - Run firefox on the host and display it on a specific profile
20+
`,
21+
Arguments: []cli.Argument{
22+
&cli.StringArg{
23+
Name: "command",
24+
Min: 1,
25+
Max: 1,
26+
Destination: &commandName,
27+
},
28+
},
29+
Flags: []cli.Flag{
30+
&cli.StringFlag{
31+
Name: "profile",
32+
Usage: "Required when multiple profiles are active",
33+
Destination: &targetProfile,
34+
},
35+
},
36+
Action: func(ctx context.Context, cmd *cli.Command) error {
37+
prof, err := profileOrActive(targetProfile)
38+
if err != nil {
39+
return err
40+
}
41+
42+
c := exec.Command(commandName)
43+
c.Env = append(c.Env, fmt.Sprintf("DISPLAY=:%d", prof.Display))
44+
out, err := c.CombinedOutput()
45+
fmt.Println(out)
46+
47+
return err
48+
},
49+
}
50+
return cmd
51+
}

cmd/cli/images.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package cli
22

33
import (
44
"context"
5+
"errors"
6+
"fmt"
57

68
"github.com/qubesome/cli/internal/images"
79
"github.com/urfave/cli/v3"
@@ -27,6 +29,15 @@ func imagesCommand() *cli.Command {
2729
},
2830
Action: func(ctx context.Context, cmd *cli.Command) error {
2931
cfg := profileConfigOrDefault(targetProfile)
32+
if cfg == nil {
33+
return errors.New("could not find qubesome config")
34+
}
35+
36+
if targetProfile != "" {
37+
if _, ok := cfg.Profile(targetProfile); !ok {
38+
return fmt.Errorf("could not find profile %q", targetProfile)
39+
}
40+
}
3041

3142
return images.Run(
3243
images.WithConfig(cfg),

cmd/cli/root.go

Lines changed: 79 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@ package cli
22

33
import (
44
"context"
5+
"errors"
6+
"fmt"
7+
"log/slog"
58
"os"
69
"path/filepath"
10+
"strings"
711

812
"github.com/qubesome/cli/internal/files"
913
"github.com/qubesome/cli/internal/log"
@@ -19,6 +23,7 @@ var (
1923
path string
2024
local string
2125
runner string
26+
commandName string
2227
debug bool
2328
)
2429

@@ -33,9 +38,17 @@ func RootCommand() *cli.Command {
3338
depsCommand(),
3439
versionCommand(),
3540
completionCommand(),
41+
hostRunCommand(),
3642
},
3743
}
3844

45+
cmd.Before = func(ctx context.Context, c *cli.Command) (context.Context, error) {
46+
if strings.EqualFold(os.Getenv("XDG_SESSION_TYPE"), "wayland") {
47+
fmt.Println("\033[33mWARN: Running qubesome in Wayland is experimental. Some features may not work as expected.\033[0m")
48+
}
49+
return ctx, nil
50+
}
51+
3952
cmd.Flags = append(cmd.Flags, &cli.BoolFlag{
4053
Name: "debug",
4154
Value: false,
@@ -70,18 +83,76 @@ func config(path string) *types.Config {
7083
}
7184

7285
func profileConfigOrDefault(profile string) *types.Config {
73-
path := files.ProfileConfig(profile)
74-
target, err := os.Readlink(path)
86+
if profile != "" {
87+
// Try to load the profile specific config.
88+
path := files.ProfileConfig(profile)
89+
target, err := os.Readlink(path)
7590

76-
var c *types.Config
77-
if err == nil {
78-
c = config(target)
91+
if err == nil {
92+
c := config(target)
93+
slog.Debug("using profile config", "path", path, "config", c)
94+
if c != nil {
95+
return c
96+
}
97+
}
7998
}
8099

81-
if c != nil {
82-
return c
100+
cfgs := activeConfigs()
101+
if len(cfgs) == 1 {
102+
c := config(cfgs[0])
103+
slog.Debug("using active profile config", "path", cfgs[0], "config", c)
104+
if c != nil && len(c.Profiles) > 0 {
105+
return c
106+
}
83107
}
84108

109+
// Try to load user-level qubesome config.
85110
path = files.QubesomeConfig()
86-
return config(path)
111+
c := config(path)
112+
slog.Debug("using user-level config", "path", path, "config", c)
113+
if c != nil && len(c.Profiles) > 0 {
114+
return c
115+
}
116+
117+
return nil
118+
}
119+
120+
func profileOrActive(profile string) (*types.Profile, error) {
121+
if profile != "" {
122+
cfg := profileConfigOrDefault(profile)
123+
prof, ok := cfg.Profile(profile)
124+
if !ok {
125+
return nil, fmt.Errorf("profile %q not active", profile)
126+
}
127+
return prof, nil
128+
}
129+
130+
cfgs := activeConfigs()
131+
if len(cfgs) > 1 {
132+
return nil, errors.New("multiple profiles active: pick one with -profile")
133+
}
134+
if len(cfgs) == 0 {
135+
return nil, errors.New("no active profile found: start one with qubesome start")
136+
}
137+
138+
f := cfgs[0]
139+
name := strings.TrimSuffix(filepath.Base(f), filepath.Ext(f))
140+
return profileOrActive(name)
141+
}
142+
143+
func activeConfigs() []string {
144+
var active []string
145+
146+
root := files.RunUserQubesome()
147+
entries, err := os.ReadDir(root)
148+
if err == nil {
149+
for _, entry := range entries {
150+
fn := entry.Name()
151+
if filepath.Ext(fn) == ".config" {
152+
active = append(active, filepath.Join(root, fn))
153+
}
154+
}
155+
}
156+
157+
return active
87158
}

cmd/cli/run.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ func runCommand() *cli.Command {
1111
cmd := &cli.Command{
1212
Name: "run",
1313
Aliases: []string{"r"},
14+
Usage: "execute workloads",
15+
Description: `Examples:
16+
17+
qubesome run chrome - Run the chrome workload on the active profile
18+
qubesome run -profile <profile> chrome - Run the chrome workload on a specific profile
19+
`,
1420
Arguments: []cli.Argument{
1521
&cli.StringArg{
1622
Name: "workload",
@@ -29,13 +35,17 @@ func runCommand() *cli.Command {
2935
Destination: &runner,
3036
},
3137
},
32-
Usage: "execute workloads",
3338
Action: func(ctx context.Context, cmd *cli.Command) error {
34-
cfg := profileConfigOrDefault(targetProfile)
39+
prof, err := profileOrActive(targetProfile)
40+
if err != nil {
41+
return err
42+
}
43+
44+
cfg := profileConfigOrDefault(prof.Name)
3545

3646
return qubesome.Run(
3747
qubesome.WithWorkload(workload),
38-
qubesome.WithProfile(targetProfile),
48+
qubesome.WithProfile(prof.Name),
3949
qubesome.WithConfig(cfg),
4050
qubesome.WithRunner(runner),
4151
qubesome.WithExtraArgs(cmd.Args().Slice()),

cmd/cli/start.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ func startCommand() *cli.Command {
1111
cmd := &cli.Command{
1212
Name: "start",
1313
Aliases: []string{"s"},
14+
Usage: "start qubesome profiles",
15+
Description: `Examples:
16+
17+
qubesome start -git https://github.com/qubesome/sample-dotfiles awesome
18+
qubesome start -git https://github.com/qubesome/sample-dotfiles i3
19+
`,
1420
Flags: []cli.Flag{
1521
&cli.StringFlag{
1622
Name: "git",
@@ -40,7 +46,6 @@ func startCommand() *cli.Command {
4046
Destination: &targetProfile,
4147
},
4248
},
43-
Usage: "start qubesome profiles",
4449
Action: func(ctx context.Context, cmd *cli.Command) error {
4550
cfg := profileConfigOrDefault(targetProfile)
4651

0 commit comments

Comments
 (0)