Support quoted commands, execute non-matching input

pull/9/head
Trevor Slocum 2019-06-28 02:11:46 -07:00
parent 6c49005342
commit 372c2be50f
7 changed files with 161 additions and 79 deletions

View File

@ -10,18 +10,12 @@ packages:
sources:
- https://git.sr.ht/~tslocum/gmenu
tasks:
- setup:
- mkdir -p $PROJECT_DIR
- mv $PROJECT_NAME $PROJECT_DIR/$PROJECT_NAME
- test-gmenu:
- cd $PROJECT_DIR/$PROJECT_NAME/cmd/gmenu
- go test
- test-gtkmenu: |
cd $PROJECT_DIR/$PROJECT_NAME/cmd/gtkmenu
go test
- build-gmenu: |
cd $PROJECT_DIR/$PROJECT_NAME/cmd/gmenu
go build
- build-gtkmenu: |
cd $PROJECT_DIR/$PROJECT_NAME/cmd/gtkmenu
go build
- setup: |
mkdir -p $PROJECT_DIR
mv $PROJECT_NAME $PROJECT_DIR/$PROJECT_NAME
- test: |
cd $PROJECT_DIR/$PROJECT_NAME/cmd/gmenu
go test -v -cover ./...
- build: |
cd $PROJECT_DIR/$PROJECT_NAME/cmd/gmenu
go build

View File

@ -11,17 +11,11 @@ sources:
- https://git.sr.ht/~tslocum/gmenu
tasks:
- setup: |
mkdir -p $PROJECT_DIR
mv $PROJECT_NAME $PROJECT_DIR/$PROJECT_NAME
- test-gmenu: |
cd $PROJECT_DIR/$PROJECT_NAME/cmd/gmenu
go test
- test-gtkmenu: |
cd $PROJECT_DIR/$PROJECT_NAME/cmd/gtkmenu
go test
- build-gmenu: |
cd $PROJECT_DIR/$PROJECT_NAME/cmd/gmenu
go build
- build-gtkmenu: |
cd $PROJECT_DIR/$PROJECT_NAME/cmd/gtkmenu
go build
mkdir -p $PROJECT_DIR
mv $PROJECT_NAME $PROJECT_DIR/$PROJECT_NAME
- test: |
cd $PROJECT_DIR/$PROJECT_NAME/cmd/gmenu
go test -v -cover ./...
- build: |
cd $PROJECT_DIR/$PROJECT_NAME/cmd/gmenu
go build

View File

@ -7,8 +7,6 @@ Desktop application launcher
## Warning: Experimental
The only platform currently supported is Linux. Windows support is planned.
Only a terminal interface has been implemented. A GTK interface is planned.
## Installation

View File

@ -5,19 +5,23 @@ package main
import (
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"path"
"strings"
"syscall"
"git.sr.ht/~tslocum/gmenu/pkg/dmenu"
"github.com/tslocum/promptui"
)
var terminal = "i3-sensible-terminal"
func init() {
log.SetFlags(0)
log.SetPrefix("gmenu: ")
promptui.SearchPrompt = ""
}
@ -68,51 +72,79 @@ func main() {
Label: "Launch:",
Active: ">{{ .Name }}",
Inactive: " {{ .Name }}",
Selected: "gmenu: Launching {{ .Name }}...",
Selected: "Launching {{ .Name }}...",
Details: `{{ "Name(s)" | faint }} {{ .Name }}{{ if .GenericName }}, {{ .GenericName }}{{ end }}
{{ "Comment" | faint }} {{ .Comment }}
{{ "Exec" | faint }} {{ .Exec }}`,
Help: "",
}
prompt := promptui.Select{
Items: desktopEntries,
IsVimMode: false,
Templates: templates,
Searcher: searcher,
StartInSearchMode: true,
Items: desktopEntries,
IsVimMode: false,
Templates: templates,
Searcher: searcher,
StartInSearchMode: true,
EnterAlwaysReturns: true,
}
selected, _, err := prompt.Run()
selected, input, err := prompt.Run()
if err != nil {
log.Fatal(err)
}
if selected < 0 {
return
}
entry := desktopEntries[selected]
// TODO: Support field codes
execSplit := strings.Split(entry.Exec, " ")
for i, arg := range execSplit {
if arg == "%F" || arg == "%f" || arg == "%U" || arg == "%u" {
execSplit[i] = ""
var (
ex string
)
if selected >= 0 {
ex = dmenu.ExpandFieldCodes(desktopEntries[selected].Exec)
if desktopEntries[selected].Terminal {
runScript, err := ioutil.TempFile("", "gmenu-*")
if err != nil {
log.Fatal(err)
}
_, err = runScript.WriteString("#!/bin/sh\n")
if err != nil {
runScript.Close()
log.Fatal(err)
}
_, err = runScript.WriteString(fmt.Sprintf("rm %s\n", runScript.Name()))
if err != nil {
runScript.Close()
log.Fatal(err)
}
_, err = runScript.WriteString(fmt.Sprintf("exec %s\n", ex))
if err != nil {
runScript.Close()
log.Fatal(err)
}
runScript.Close()
if err := os.Chmod(runScript.Name(), 0744); err != nil {
log.Fatal(err)
}
ex = fmt.Sprintf(`%s -e '%s'`, terminal, runScript.Name())
} else {
ex = fmt.Sprintf(`/bin/bash -i -c '%s'`, strings.ReplaceAll(ex, `'`, `'\''`))
}
}
var args []string
if len(execSplit) > 1 {
args = execSplit[1:]
}
cmd := exec.Command(execSplit[0], args...)
if entry.Terminal {
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err = cmd.Run()
} else {
err = cmd.Start()
ex = fmt.Sprintf(`/bin/bash -i -c '%s'`, strings.ReplaceAll(input, `'`, `'\''`))
}
command, args, err := dmenu.CommandNameAndArgs(ex)
if err != nil {
log.Fatal(err)
}
cmd := exec.Command(command, args...)
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true, Pgid: 0}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err = cmd.Start()
if err != nil {
log.Fatal(err)
}

5
go.mod
View File

@ -4,7 +4,6 @@ go 1.12
require (
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
github.com/kr/pty v1.1.5 // indirect
github.com/mattn/go-isatty v0.0.8 // indirect
github.com/tslocum/promptui v0.3.3-0.20190626143017-6cce979adef8
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/tslocum/promptui v0.3.4-0.20190628082230-cf53eafff9c5
)

14
go.sum
View File

@ -1,5 +1,4 @@
github.com/alecthomas/gometalinter v2.0.11+incompatible/go.mod h1:qfIpQGGz3d+NmgyPBqv+LSh50emm1pt72EtcX2vKYQk=
github.com/alecthomas/gometalinter v2.0.12+incompatible/go.mod h1:qfIpQGGz3d+NmgyPBqv+LSh50emm1pt72EtcX2vKYQk=
github.com/alecthomas/gometalinter v3.0.0+incompatible/go.mod h1:qfIpQGGz3d+NmgyPBqv+LSh50emm1pt72EtcX2vKYQk=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
@ -12,9 +11,10 @@ github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf/go.mod h1:RpwtwJQFrIE
github.com/gordonklaus/ineffassign v0.0.0-20180909121442-1003c8bd00dc/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU=
github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU=
github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw=
github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
@ -24,18 +24,12 @@ github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRU
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/tsenart/deadcode v0.0.0-20160724212837-210d2dc333e9/go.mod h1:q+QjxYvZ+fpjMXqs+XEriussHjSYqeXVnAdSV1tkMYk=
github.com/tslocum/promptui v0.3.3-0.20190626142258-8da0c0f6cdc6 h1:ApJCfIVMWJ1AwC0IkFO6vx7B2pkGkaearWQdkZtb/5s=
github.com/tslocum/promptui v0.3.3-0.20190626142258-8da0c0f6cdc6/go.mod h1:ib2idig9p59EUDQJwRIN3/JHD8ThqsUu9fnPuA4zspA=
github.com/tslocum/promptui v0.3.3-0.20190626143017-6cce979adef8 h1:OkqQt5hcU507iYyocFea4Rz18xuPoKdUOUD7SQMtc1s=
github.com/tslocum/promptui v0.3.3-0.20190626143017-6cce979adef8/go.mod h1:ib2idig9p59EUDQJwRIN3/JHD8ThqsUu9fnPuA4zspA=
github.com/tslocum/promptui v0.3.4-0.20190628082230-cf53eafff9c5 h1:ngqI2eodloV7WADE+TRxuXo9lrPLXnsn+7nQvwG8kBY=
github.com/tslocum/promptui v0.3.4-0.20190628082230-cf53eafff9c5/go.mod h1:ib2idig9p59EUDQJwRIN3/JHD8ThqsUu9fnPuA4zspA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/tools v0.0.0-20181122213734-04b5d21e00f1/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@ -8,7 +8,8 @@ import (
"os"
"path/filepath"
"strings"
"unicode"
"github.com/kballard/go-shellquote"
)
// TODO: Support Type=URL
@ -28,6 +29,17 @@ var (
entryNoDisplay = []byte("nodisplay=true")
)
var quotes = map[string]string{
`%%`: `%`,
`\\\\ `: `\\ `,
`\\\\` + "`": `\\` + "`",
`\\\\$`: `\\$`,
`\\\\(`: `\\(`,
`\\\\)`: `\\)`,
`\\\\\`: `\\\`,
`\\\\\\\\`: `\\\\`,
}
type DesktopEntry struct {
Name string
GenericName string
@ -72,6 +84,64 @@ func ScanEntries(dirs []string) ([]*DesktopEntry, error) {
return entries, nil
}
func UnquoteExec(ex string) string {
for qs, qr := range quotes {
ex = strings.ReplaceAll(ex, qs, qr)
}
return ex
}
// TODO
func ExpandFieldCodes(ex string) string {
ex = strings.ReplaceAll(ex, "%F", "")
ex = strings.ReplaceAll(ex, "%f", "")
ex = strings.ReplaceAll(ex, "%U", "")
ex = strings.ReplaceAll(ex, "%u", "")
return ex
}
func CommandNameAndArgs(ex string) (string, []string, error) {
execLen := len(ex)
if execLen == 0 {
return "", nil, nil
} else if execLen <= 2 || ex[0] != '"' {
split := strings.SplitN(ex, " ", 2)
if len(split) == 1 {
return split[0], nil, nil
}
args, err := shellquote.Split(split[1])
if err != nil {
return "", nil, err
}
return split[0], args, nil
} else {
endQuote := strings.IndexRune(ex[1:], '"')
if endQuote > 0 {
var argsAfterQuote string
if execLen > (endQuote + 2) {
if ex[endQuote+2] == ' ' {
argsAfterQuote = ex[endQuote+3:]
} else {
argsAfterQuote = ex[endQuote+2:]
}
}
args, err := shellquote.Split(argsAfterQuote)
if err != nil {
return "", nil, err
}
return ex[1 : endQuote+1], args, nil
} else {
return ex, nil, nil
}
}
}
func scanDirectory(path string, f os.FileInfo, err error) error {
if f.IsDir() || !strings.HasSuffix(strings.ToLower(path), ".desktop") {
return nil
@ -89,8 +159,9 @@ func scanDirectory(path string, f os.FileInfo, err error) error {
var foundHeader bool
for scanner.Scan() {
scannedBytes = bytes.TrimRightFunc(scanner.Bytes(), unicode.IsSpace)
scannedBytes = bytes.TrimSpace(scanner.Bytes())
scannedBytesLen = len(scannedBytes)
if scannedBytesLen == 0 || scannedBytes[0] == byte('#') {
continue
} else if scannedBytes[0] == byte('[') {
@ -114,7 +185,7 @@ func scanDirectory(path string, f os.FileInfo, err error) error {
} else if scannedBytesLen >= 5 && bytes.EqualFold(scannedBytes[0:5], entryIcon) {
entry.Icon = string(scannedBytes[5:])
} else if scannedBytesLen >= 5 && bytes.EqualFold(scannedBytes[0:5], entryExec) {
entry.Exec = string(scannedBytes[5:])
entry.Exec = UnquoteExec(string(scannedBytes[5:]))
} else if scannedBytesLen >= 13 && bytes.EqualFold(scannedBytes, entryTerminal) {
entry.Terminal = true
} else if scannedBytesLen >= 14 && bytes.EqualFold(scannedBytes, entryNoDisplay) {