8000 Implement subscriptions by jeremija · Pull Request #2 · jeremija/wl-gammarelay · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Implement subscriptions #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jan 23, 2022
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
12 changes: 5 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,6 @@ disabled using the `--no-daemon/-D` flag, but if the daemon isn't already runnin
in the background the requests will fail. After the daemon starts up, the
temperature and brightess will be set to the desired levels.

The daemon will also write the last color temperature and brightness to a
history file which can then be tailed to display the value(s) in `waybar` or
`i3status-rust`. The path can be set using the `--history/-H` flag, which should
be set to an empty string to disable this functionality.

All other invocations act as clients only send requests via unix domain socket.
The path of the socket for both the daemon and the client can be controlled
using the `--sock/-s` flag.
Expand All @@ -31,6 +26,9 @@ when set to an absolute values. Relative changes can be specified by adding a
The `--brigtness/-b` flag behaves similarly to temperature, only its range is
`[0, 1.0]` and it accepts floats.

The `--subscribe/-S` flag can be used to subscribe to certain changes.
Currently only `color` is supported.

Below are some examples on how this utility can be used to change the color
temperature via keybindings in `swaywm`:

Expand All @@ -47,8 +45,8 @@ Sample configuration for `waybar`:
```config
"modules-right": ["custom/wl-gammarelay"],
"custom/wl-gammarelay": {
"format": "{} ",
"exec": "tail -F /tmp/.wl-gammarelay.hist 2>/dev/null"
"format": "{} ",
"exec": "wl-gammarelay --subscribe color | jq --unbuffered --compact-output -r -c '.updates[] | select(.key == \"color\") | .color | .temperature + \" \" + .brightness'"
}
```

Expand Down
261 changes: 214 additions & 47 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net"
"os"
"os/signal"
"path"
"syscall"
"time"

"github.com/jeremija/wl-gammarelay/display"
"github.com/jeremija/wl-gammarelay/service"
"github.com/jeremija/wl-gammarelay/types"
"github.com/spf13/pflag"
Expand All @@ -26,11 +29,14 @@ type Arguments struct {
SocketPath string
HistoryPath string

NoStartDaemon bool
NoStartDaemon bool
ReconnectTimeout time.Duration

Temperature string
Brightness string

Subscribe []string

Version bool
Verbose bool
}
Expand Down Expand Up @@ -67,19 +73,18 @@ func parseArgs(argsSlice []string) (Arguments, error) {
fs.PrintDefaults()
}

tempDir := os.TempDir()

defaultHistoryPath := path.Join(tempDir, ".wl-gammarelay.hist")
defaultSocketPath := path.Join(getSocketDir(), "wl-gammarelay.sock")

fs.StringVarP(&args.HistoryPath, "history", "H", defaultHistoryPath, "History file to use")
fs.StringVarP(&args.SocketPath, "sock", "s", defaultSocketPath, "Unix domain socket path for RPC")

fs.StringVarP(&args.Temperature, "temperature", "t", "", "Color temperature to set, neutral is 6500.")
fs.StringVarP(&args.Brightness, "brightness", "b", "", "Brightness to set, max is 1.0")

fs.BoolVarP(&args.NoStartDaemon, "no-daemon", "D", false, "Do not start daemon if not running")

fs.StringSliceVarP(&args.Subscribe, "subscribe", "S", nil, "Subscribe to updates. Currently supported: color")
fs.DurationVarP(&args.ReconnectTimeout, "reconnect-timeout", "T", 5*time.Second, "Time to reconnect after subscribe fails. Set to 0 to disable.")

fs.BoolVarP(&args.Version, "version", "V", false, "Print version and exit")
fs.BoolVarP(&args.Verbose, "verbose", "v", false, "Print client socket request and response messages")

Expand All @@ -90,6 +95,14 @@ func parseArgs(argsSlice []string) (Arguments, error) {
return args, nil
}

func writeRequest(request types.Request) {
json.NewEncoder(os.Stdout).Encode(request)
}

func writeResponse(response types.Response) {
json.NewEncoder(os.Stdout).Encode(response)
}

// main is a test function for proof-of-concept.
func main() {
args, err := parseArgs(os.Args)
Expand All @@ -100,93 +113,247 @@ func main() {
panic(err)
}

if err := main2(args); err != nil {
panic(err)
}
}

func main2(args Arguments) error {
if args.Version {
fmt.Println(Version)

if CommitHash != "" {
fmt.Println(CommitHash)
}

return
return nil
}

if err := main2(args); err != nil {
panic(err)
}
}

func main2(args Arguments) error {
ctx := context.Background()

// We need to handle these events so that the listener removes the socket
// file gracefully, otherwise the daemon might not start successfully next
// time.
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM, syscall.SIGPIPE)
defer cancel()

var startedDaemon bool

_, err := os.Stat(args.SocketPath)
if err != nil && !args.NoStartDaemon {
service := service.New(service.Params{
SocketPath: args.SocketPath,
HistoryPath: args.HistoryPath,
Verbose: args.Verbose,
})

if err := service.Listen(); err != nil {
service, err := newDaemon(args.SocketPath, args.HistoryPath, args.Verbose)
if err != nil {
return fmt.Errorf("failed to start service: %w", err)
}

defer service.Close()

log.Printf("Started daemon\n")

go func() {
if err := service.Serve(ctx); err != nil {
log.Printf("Serve done: %s\n", err)
go service.Serve(ctx)

startedDaemon = true
}

switch {
case args.Temperature != "" || args.Brightness != "":
if err := setTemperature(args); err != nil {
return fmt.Errorf("set temperature failed: %w", err)
}

case args.Subscribe != nil:
if err := subscribe(ctx, args); err != nil {
if errors.Is(err, ctx.Err()) {
return nil
}
}()
} else {
// So we don't block at the end.
cancel()

return fmt.Errorf("subscribe failed: %w", err)
}
}

// Act as a client.
color := args.Color()
// If we started the server, keep running until the context is canceled, otherwise bail.
if startedDaemon {
<-ctx.Done()
}

conn, err := net.Dial("unix", args.SocketPath)
return nil
}

func newDaemon(socketPath string, historyPath string, verbose bool) (*service.Service, error) {
display, err := display.New()
if err != nil {
return fmt.Errorf("dial unix socket: %w", err)
return nil, fmt.Errorf("failed to create display: %w", err)
}

defer conn.Close()
listener, err := net.Listen("unix", socketPath)
if err != nil {
return nil, fmt.Errorf("failed to listen: %w", err)
}

request, err := json.Marshal(types.Request{
return service.New(service.Params{
Listener: listener,
Display: display,
HistoryPath: historyPath,
Verbose: verbose,
}), nil
}

func setTemperature(args Arguments) error {
cl, err := dialClient(args.SocketPath, args.Verbose)
if err != nil {
return fmt.Errorf("dialling client: %w", err)
}

defer cl.Close()

color := args.Color()

request := types.Request{
Color: &color,
})
}

if err := cl.Write(request); err != nil {
return fmt.Errorf("writing request: %w", err)
}

_, err = cl.Read()
if err != nil {
return fmt.Errorf("encoding request: %w", err)
return fmt.Errorf("reading response: %w", err)
}

if args.Verbose {
fmt.Println(string(request))
return nil
}

func subscribe(ctx context.Context, args Arguments) error {
connectAndReadLoop := func() error {
cl, err := dialClient(args.SocketPath, args.Verbose)
if err != nil {
return fmt.Errorf("dialling client: %w", err)
}

defer cl.Close()

keys := make([]types.SubscriptionKey, len(args.Subscribe))

for i, key := range args.Subscribe {
keys[i] = types.SubscriptionKey(key)
}

request := types.Request{
Subscribe: keys,
}

if err := cl.Write(request); err != nil {
return fmt.Errorf("writing request: %w", err)
}

doneCh := make(chan struct{})
defer close(doneCh)

go func() {
select {
case <-ctx.Done():
cl.Close()
case <-doneCh:
// So we don't leave dangling goroutines on reconnect
}
}()

for {
response, err := cl.Read()
if err != nil {
return fmt.Errorf("read error: %w", err)
}

writeResponse(response)
}
}

if err = json.NewEncoder(conn).Encode(json.RawMessage(request)); err != nil {
return fmt.Errorf("encoding request: %w", err)
for {
// connectAndReadLoop always returns an error when it's done.
err := connectAndReadLoop()

// If the cause was context, we're done.
if ctx.Err() != nil {
return fmt.Errorf("context done: %w", err)
}

if errors.Is(err, io.EOF) {
continue
}

if args.ReconnectTimeout <= 0 {
return fmt.Errorf("subscribe failed: %w", err)
}

log.Printf("Dial failed, reconnecting in: %s\n", args.ReconnectTimeout)

timer := time.NewTimer(args.ReconnectTimeout)

select {
case <-timer.C:
case <-ctx.Done():
timer.Stop()
return fmt.Errorf("context done: %w", ctx.Err())
}
}
}

type client struct {
conn net.Conn

var res json.RawMessage
decoder *json.Decoder
encoder *json.Encoder

if err := json.NewDecoder(conn).Decode(&res); err != nil {
return fmt.Errorf("decoding response: %w", err)
verbose bool
}

func dialClient(socketPath string, verbose bool) (*client, error) {
conn, err := net.Dial("unix", socketPath)
if err != nil {
return nil, fmt.Errorf("dial unix socket: %w", err)
}

if args.Verbose {
fmt.Println(string(res))
return &client{
conn: conn,

encoder: json.NewEncoder(conn),
decoder: json.NewDecoder(conn),

verbose: verbose,
}, nil
}

func (c *client) Close() error {
if err := c.conn.Close(); err != nil {
return fmt.Errorf("failed to close: %w", err)
}

conn.Close()
return nil
}

func (c *client) Write(request types.Request) error {
if c.verbose {
writeRequest(request)
}

// If we started the server, keep running until the context is canceled.
<-ctx.Done()
if err := c.encoder.Encode(request); err != nil {
return fmt.Errorf("failed to encode request: %w", err)
}

return nil
}

func (c *client) Read() (types.Response, error) {
var response types.Response

if err := c.decoder.Decode(&response); err != nil {
return types.Response{}, fmt.Errorf("failed to decode response: %w", err)
}

if c.verbose {
writeResponse(response)
}

return response, nil
}
Loading
0