Skip to content

Commit

Permalink
Piping (#31)
Browse files Browse the repository at this point in the history
* added comments to background case

* added support for piping; added -i flag
  • Loading branch information
dakyskye authored Oct 30, 2020
1 parent fa52647 commit 753c4b2
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 77 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,14 @@ and copy `dxhd` executable file to somewhere in your `$PATH`
* calculating the time parsing a config file took (`dxhd -p`)
* editing config files quickly (`dxhd -e i3.py`)
* running as a daemon (`dxhd -b`)
* running interactively (`dxhd -i`)
* support for any shell scripting language (sh, bash, ksh, zsh, python, perl etc.) given as a shebang
* support for global variable declarations in a config
* support for scripting, as much as a user wishes!

## Configuration

The default config file is located at `~/.config/dxhd/dxhd.sh`, however, dxhd can read a file from any path, by passing it to `-c`:
The default config file is `~/.config/dxhd/dxhd.sh`, however, dxhd can read a file from any path, by passing it to `-c`:

```sh
dxhd -c /my/custom/path/to/a/config/file
Expand Down
180 changes: 137 additions & 43 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
package main

import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"os/signal"
"path/filepath"
"runtime"
"strconv"
"strings"
"syscall"
"time"

Expand Down Expand Up @@ -67,6 +69,21 @@ func main() {
logger.L().Fatalln("dxhd is only supported on linux")
}

stdin := new([]byte)

stat, err := os.Stdin.Stat()
if err != nil {
logger.L().WithError(err).Fatal("can not stat stdin")
}
if stat.Mode()&os.ModeCharDevice == 0 {
*stdin, err = ioutil.ReadAll(os.Stdin)
if err != nil {
logger.L().WithError(err).Fatal("can not read the stdin")
}
} else {
stdin = nil
}

opts, err := options.Parse()
if err != nil {
logger.L().Fatalln(err)
Expand All @@ -86,9 +103,15 @@ func main() {
exit = true
}

if opts.Background {
cmd := exec.Command("/bin/sh")
cmd.Stdin = strings.NewReader(strings.Join(os.Args, " "))
runInBackground := func(data *[]byte) (err error) {
exc, err := os.Executable()
if err != nil {
logger.L().WithError(err).Fatal("can not get the executable")
}
cmd := exec.Command(exc, os.Args...)
if data != nil {
cmd.Stdin = bytes.NewReader(*data)
}
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.SysProcAttr = &syscall.SysProcAttr{
Expand All @@ -100,30 +123,49 @@ func main() {
logger.L().WithError(err).Fatal("can not run dxhd in the background")
}
time.Sleep(time.Microsecond * 10)
return
}

if opts.Background && !opts.Interactive {
os.Exit(1)
err = runInBackground(stdin)
if err != nil {
logger.L().WithError(err).Fatal("can not run dxhd in the background")
}
os.Exit(0)
}

if opts.Edit != nil {
findEditor := func() (ed string, e error) {
editor := os.Getenv("EDITOR")
editors := []string{editor, "nano", "nvim", "vim", "vi"}
for _, ed := range editors {
editor, err = exec.LookPath(ed)
if err == nil {
editors := [5]string{editor, "nano", "nvim", "vim", "vi"}
for _, ed = range editors {
ed, e = exec.LookPath(ed)
if e == nil {
break
}
}
if editor != "" {
_, configDir, _ := config.GetDefaultConfigPath()
if *opts.Edit == "" {
*opts.Edit = "dxhd.sh"
}
path := filepath.Join(configDir, *opts.Edit)
err = syscall.Exec(editor, []string{editor, path}, os.Environ())
if err != nil {
logger.L().WithError(err).WithFields(logrus.Fields{"editor": editor, "path": path}).Fatal("cannot invoke editor")
}
} else {
logger.L().Fatal("cannot find a suitable editor to open, please set one in $EDTIOR")
if e != nil {
e = errors.New("no text editor was found installed")
}
return
}

if opts.Edit != nil {
editor, err := findEditor()
if err != nil {
logger.L().WithError(err).Fatal("can not find a suitable editor to use")
}
_, configDir, _ := config.GetDefaultConfigPath()
if *opts.Edit == "" {
*opts.Edit = "dxhd.sh"
}
path := filepath.Join(configDir, *opts.Edit)
cmd := exec.Command(editor, path)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err = cmd.Run()
if err != nil {
logger.L().WithError(err).WithFields(logrus.Fields{"editor": editor, "path": path}).Fatal("cannot invoke editor")
}
exit = true
}
Expand All @@ -133,28 +175,71 @@ func main() {
validPath bool
)

if opts.Config != nil {
if validPath, err = config.IsPathToConfigValid(*opts.Config); !(err == nil && validPath) {
logger.L().WithFields(logrus.Fields{
"path": *opts.Config,
"valid": validPath,
}).WithError(err).Fatal("path to the config is not valid")
}
configFilePath = *opts.Config
} else {
configFilePath, _, err = config.GetDefaultConfigPath()
if err != nil {
logger.L().WithError(err).Fatal("can not get config path")
}
if stdin == nil {
if opts.Interactive && opts.Config == nil {
editor, err := findEditor()
if err != nil {
logger.L().WithError(err).Fatal("can not find a suitable editor to use")
}
tmp, err := ioutil.TempFile("/tmp", "dxhd")
if err != nil {
logger.L().WithError(err).Fatal("can not create a temp file for interactive l")
}

_, err = tmp.WriteString("#!/bin/sh")
if err != nil {
logger.L().WithError(err).WithField("file", tmp.Name()).Warn("can not write default shebang to temp file")
}

cmd := exec.Command(editor, tmp.Name())
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err = cmd.Run()
if err != nil {
logger.L().WithError(err).WithFields(logrus.Fields{"editor": editor, "path": tmp.Name()}).Fatal("cannot invoke editor")
}

stdin = new([]byte)
*stdin, err = ioutil.ReadFile(tmp.Name()) // ioutil.ReadAll(tmp) omits the shebang
if err != nil {
logger.L().WithError(err).WithField("file", tmp).Fatal("can not read file")
}

if validPath, err = config.IsPathToConfigValid(configFilePath); !(err == nil && validPath) {
if os.IsNotExist(err) {
err = config.CreateDefaultConfig()
err = os.Remove(tmp.Name())
if err != nil {
logger.L().WithError(err).Warn("can not delete temp file dxhd created")
}

if opts.Background {
err = runInBackground(stdin)
if err != nil {
logger.L().WithField("path", configFilePath).Fatal("can not create default config")
logger.L().WithError(err).Fatal("can not run dxhd in the background")
}
os.Exit(0)
}
} else if opts.Config != nil {
if validPath, err = config.IsPathToConfigValid(*opts.Config); !(err == nil && validPath) {
logger.L().WithFields(logrus.Fields{
"path": *opts.Config,
"valid": validPath,
}).WithError(err).Fatal("path to the config is not valid")
}
configFilePath = *opts.Config
} else {
configFilePath, _, err = config.GetDefaultConfigPath()
if err != nil {
logger.L().WithError(err).Fatal("can not get config path")
}

if validPath, err = config.IsPathToConfigValid(configFilePath); !(err == nil && validPath) {
if os.IsNotExist(err) {
err = config.CreateDefaultConfig()
if err != nil {
logger.L().WithField("path", configFilePath).Fatal("can not create default config")
}
} else {
logger.L().WithFields(logrus.Fields{"path": configFilePath, "valid": validPath}).WithError(err).Fatal("path to the config is not valid")
}
} else {
logger.L().WithFields(logrus.Fields{"path": configFilePath, "valid": validPath}).WithError(err).Fatal("path to the config is not valid")
}
}
}
Expand All @@ -170,7 +255,12 @@ func main() {
startTime = time.Now()
}

shell, globals, err = parser.Parse(configFilePath, &data)
if stdin != nil {
shell, globals, err = parser.Parse(*stdin, &data)
*stdin = []byte("")
} else {
shell, globals, err = parser.Parse(configFilePath, &data)
}
if err != nil {
logger.L().WithField("file", configFilePath).WithError(err).Fatal("failed to parse config")
}
Expand Down Expand Up @@ -245,7 +335,7 @@ func main() {
// infinite loop - if user sends USR signal, reload configration (so, continue loop), otherwise, exit
toplevel:
for {
if len(data) == 0 {
if len(data) == 0 && stdin == nil {
shell, globals, err = parser.Parse(configFilePath, &data)
if err != nil {
logger.L().WithField("file", configFilePath).WithError(err).Fatal("failed to parse config")
Expand Down Expand Up @@ -279,10 +369,14 @@ toplevel:
}
continue
case sig := <-signals:
if (sig == syscall.SIGUSR1 || sig == syscall.SIGUSR2) && stdin != nil {
logger.L().Debug("user defined signal received, but not reloading, as dxhd's using memory config")
continue
}
keybind.Detach(X, X.RootWin())
mousebind.Detach(X, X.RootWin())
xevent.Quit(X)
if sig == syscall.SIGUSR1 || sig == syscall.SIGUSR2 {
if (sig == syscall.SIGUSR1 || sig == syscall.SIGUSR2) && stdin == nil {
logger.L().Debug("user defined signal received, reloading")
continue toplevel
}
Expand Down
36 changes: 20 additions & 16 deletions options/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@ import (
)

type Options struct {
Help bool
Kill bool
Reload bool
Version bool
DryRun bool
ParseTime bool
Background bool
Config *string
Edit *string
Help bool
Kill bool
Reload bool
Version bool
DryRun bool
ParseTime bool
Background bool
Interactive bool
Config *string
Edit *string
}

var OptionsToPrint = `
Expand All @@ -28,7 +29,8 @@ var OptionsToPrint = `
-p, --parse-time Prints how much time parsing a config took
-r, --reload Reloads every running instances of dxhd
-v, --version Prints current version of program
-e, --edit [file] Shortcut to edit a file in dxhd's config folder. Opens dxhd.sh if file is empty`
-e, --edit [file] Shortcut to edit a file in dxhd's config folder. Opens dxhd.sh if file is empty
-i, --interactive Opens a temporary file for temporary bindings to run`

func Parse() (opts Options, err error) {
osArgs := os.Args[1:]
Expand Down Expand Up @@ -64,8 +66,7 @@ func Parse() (opts Options, err error) {
opts.ParseTime = true
case opt == "background":
opts.Background = true
os.Args[1+in] = ""
return
os.Args[1+in] = "" // remove --background
case opt == "config":
opts.Config, err = readNextArg(in, false)
if err != nil {
Expand All @@ -77,6 +78,8 @@ func Parse() (opts Options, err error) {
*opts.Config = strings.TrimPrefix(opt, "config=")
case opt == "version":
opts.Version = true
case opt == "interactive":
opts.Interactive = true
case opt == "edit":
opts.Edit, err = readNextArg(in, true)
if err != nil {
Expand Down Expand Up @@ -115,12 +118,11 @@ func Parse() (opts Options, err error) {
opts.ParseTime = true
case "b":
opts.Background = true
if len(osArg) == 2 {
if len(osArg) == 2 { // means b is the only flag with dash prefix
os.Args[1+in] = ""
} else {
os.Args[1+in] = os.Args[1+in][0:1+i] + os.Args[1+in][2+i:]
} else { // means it's a flag combo
os.Args[1+in] = os.Args[1+in][0:1+i] + os.Args[1+in][2+i:] // remove b from the combo
}
return
case "c":
opts.Config, err = readNextArg(in, false)
if err != nil {
Expand All @@ -141,6 +143,8 @@ func Parse() (opts Options, err error) {
} else {
skip = true
}
case "i":
opts.Interactive = true
default:
err = fmt.Errorf("%s in %s is not a valid option", string(r), osArg)
return
Expand Down
Loading

0 comments on commit 753c4b2

Please sign in to comment.