1package cli
2
3import (
4 "context"
5 "encoding/json"
6 "flag"
7 "fmt"
8 "os"
9 "runtime/debug"
10 "strconv"
11
12 "github.com/bnema/neferwl/internal/adapters/config"
13 "github.com/bnema/neferwl/internal/adapters/statefile"
14 "github.com/bnema/neferwl/internal/ports"
15)
16
17var validateConfigCmd = Command{
18 Name: "validate-config",
19 Args: "[path]",
20 Summary: "Check a config file without starting the compositor.",
21 Setup: func(*flag.FlagSet) Runner {
22 return func(_ context.Context, env Env, args []string) error {
23 if len(args) > 1 {
24 return usagef("expected at most one path, got %d", len(args))
25 }
26 path := ""
27 if len(args) == 1 {
28 path = args[0]
29 }
30 path, _, warnings, err := loadConfig(path)
31 if err != nil {
32 return err
33 }
34 for _, w := range warnings {
35 fmt.Fprintf(env.Stderr, "%s:%s\n", path, w)
36 }
37 if len(warnings) > 0 {
38 return fmt.Errorf("%d warning(s)", len(warnings))
39 }
40 fmt.Fprintf(env.Stdout, "ok: %s\n", path)
41 return nil
42 }
43 },
44}
45
46// loadConfig reads the config at path, or the XDG default when path is
47// empty; a missing default file gives the defaults. It returns the path read.
48func loadConfig(path string) (string, ports.Config, []config.Warning, error) {
49 if path == "" {
50 cfg, warnings, err := config.LoadDefault()
51 return config.DefaultPath(), cfg, warnings, err
52 }
53 cfg, warnings, err := config.Load(path)
54 return path, cfg, warnings, err
55}
56
57// stateCmd reads the file named by $NEFERWL_STATE, else the one of
58// $WAYLAND_DISPLAY.
59var stateCmd = Command{
60 Name: "state",
61 Args: "[output-of <pid>]",
62 Summary: "Print the session state, or the output showing a process's window, as JSON.",
63 Setup: func(*flag.FlagSet) Runner {
64 return func(_ context.Context, env Env, args []string) error {
65 pid := 0
66 switch {
67 case len(args) == 0:
68 case len(args) == 2 && args[0] == "output-of":
69 n, err := strconv.Atoi(args[1])
70 if err != nil || n <= 0 {
71 return usagef("invalid pid %q", args[1])
72 }
73 pid = n
74 default:
75 return usagef("unexpected arguments: %v", args)
76 }
77 path := os.Getenv(statefile.Env)
78 if path == "" {
79 var err error
80 if path, err = statefile.Path(os.Getenv("XDG_RUNTIME_DIR"), os.Getenv("WAYLAND_DISPLAY")); err != nil {
81 return err
82 }
83 }
84 st, err := statefile.Read(path)
85 if err != nil {
86 return err
87 }
88 var out any = st
89 if pid != 0 {
90 if out, err = statefile.OutputOf(st, pid); err != nil {
91 return err
92 }
93 }
94 data, err := json.Marshal(out)
95 if err != nil {
96 return err
97 }
98 _, err = env.Stdout.Write(append(data, '\n'))
99 return err
100 }
101 },
102}
103
104var versionCmd = Command{
105 Name: "version",
106 Summary: "Print the neferwl version.",
107 Setup: func(*flag.FlagSet) Runner {
108 return func(_ context.Context, env Env, args []string) error {
109 if len(args) != 0 {
110 return usagef("unexpected arguments: %v", args)
111 }
112 fmt.Fprintln(env.Stdout, buildVersion(env.Version))
113 return nil
114 }
115 },
116}
117
118// buildVersion prefers the packaged version, then the module version.
119func buildVersion(packaged string) string {
120 if packaged != "" {
121 return packaged
122 }
123 info, ok := debug.ReadBuildInfo()
124 if !ok {
125 return "unknown"
126 }
127 if info.Main.Version == "" {
128 return "(devel)"
129 }
130 return info.Main.Version
131}