commit 72f5fa21c75767f9a7a44fb55e0bdb04b81bfb94 Author: milarin Date: Sun Mar 19 13:08:11 2023 +0100 initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..808aa70 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# slash + +Linux shell \ No newline at end of file diff --git a/ansi_seq.go b/ansi_seq.go new file mode 100644 index 0000000..d8b8957 --- /dev/null +++ b/ansi_seq.go @@ -0,0 +1,62 @@ +package main + +import ( + "os" + "os/exec" +) + +func doItRaw() { + rawMode := exec.Command("/bin/stty", "raw", "-echo") + rawMode.Stdin = os.Stdin + err := rawMode.Run() + if err != nil { + panic(err) + } +} + +func cook() { + cookedMode := exec.Command("/bin/stty", "-raw", "echo") + cookedMode.Stdin = os.Stdin + err := cookedMode.Run() + if err != nil { + panic(err) + } +} + +func clearEOL() { + os.Stdout.Write([]byte{0x1b, 0x5b, 0x4b}) +} + +func clear() { + os.Stdout.Write([]byte{0x1b, 0x5b, 0x48, 0x1b, 0x5b, 0x32, 0x4a, 0x1b, 0x5b, 0x33, 0x4a}) +} + +func goUp() { + os.Stdout.Write([]byte{0x1b, 0x5b, 0x41}) +} + +func goLeft() { + os.Stdout.Write([]byte{0x08}) +} + +// func save() { +// cmd := exec.Command("tput", "sc") +// cmd.Stdin = os.Stdin +// cmd.Stdout = os.Stdout +// cmd.Stderr = os.Stderr +// err := cmd.Run() +// if err != nil { +// panic(err) +// } +// } + +// func restore() { +// cmd := exec.Command("tput", "rc") +// cmd.Stdin = os.Stdin +// cmd.Stdout = os.Stdout +// cmd.Stderr = os.Stderr +// err := cmd.Run() +// if err != nil { +// panic(err) +// } +// } diff --git a/colorstring.go b/colorstring.go new file mode 100644 index 0000000..7500522 --- /dev/null +++ b/colorstring.go @@ -0,0 +1,46 @@ +package main + +import ( + "unicode/utf8" + + "github.com/fatih/color" +) + +type ColorString struct { + color string + raw string +} + +func (s *ColorString) Append(str string, attrs ...color.Attribute) *ColorString { + s.color += color.New(attrs...).Sprint(str) + s.raw += str + return s +} + +func (s *ColorString) AppendColorString(str ...*ColorString) *ColorString { + for _, o := range str { + s.color += o.color + s.raw += o.raw + } + return s +} + +func (s *ColorString) Equals(other *ColorString) bool { + return s.color == other.color && s.raw == other.raw +} + +func (s *ColorString) Len() int { + return utf8.RuneCountInString(s.raw) +} + +func (s *ColorString) ColorLen() int { + return utf8.RuneCountInString(s.color) +} + +func (s *ColorString) String() string { + return s.color +} + +func (s *ColorString) Raw() string { + return s.raw +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c39475a --- /dev/null +++ b/go.mod @@ -0,0 +1,16 @@ +module git.milar.in/slash/shell + +go 1.17 + +require ( + git.milar.in/milarin/adverr v1.1.0 + github.com/fatih/color v1.12.0 + golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 +) + +require ( + github.com/mattn/go-colorable v0.1.8 // indirect + github.com/mattn/go-isatty v0.0.12 // indirect + golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect + golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1fcbe05 --- /dev/null +++ b/go.sum @@ -0,0 +1,20 @@ +git.milar.in/milarin/adverr v1.1.0 h1:jD9WnOvs40lfMhvqQ7cllOaRJNBMWr1f07/s9jAadp0= +git.milar.in/milarin/adverr v1.1.0/go.mod h1:joU9sBb7ySyNv4SpTXB0Z4o1mjXsArBw4N27wjgzj9E= +github.com/fatih/color v1.12.0 h1:mRhaKNwANqRgUBGKmnI5ZxEk7QXmjQeCcuYFMX2bfcc= +github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= +github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/handle.go b/handle.go new file mode 100644 index 0000000..e534b7b --- /dev/null +++ b/handle.go @@ -0,0 +1,46 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "strings" + + "git.milar.in/milarin/adverr" +) + +func handle(input string) { + input = strings.TrimSpace(input) + + if input == "" { + return + } + + if input == "up" { + goUp() + return + } + + if strings.HasPrefix(input, "cd ") { + os.Chdir(strings.Split(input, " ")[1]) + return + } + + cook() + defer doItRaw() + + parts := strings.Split(input, " ") + cmd := exec.Command(parts[0], parts[1:]...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Start(); err != nil { + fmt.Println(adverr.Wrap("Could not start process", err)) + return + } + + if err := cmd.Wait(); err != nil { + fmt.Println(adverr.Wrap("Could not wait for process to complete", err)) + return + } +} diff --git a/input.go b/input.go new file mode 100644 index 0000000..b2bc27b --- /dev/null +++ b/input.go @@ -0,0 +1,19 @@ +package main + +import ( + "os" +) + +func handleInput() <-chan byte { + ch := make(chan byte) + + go func(ch chan byte) { + data := make([]byte, 1) + for _, err := os.Stdin.Read(data); err == nil; _, err = os.Stdin.Read(data) { + ch <- data[0] + } + close(ch) + }(ch) + + return ch +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..3e716ac --- /dev/null +++ b/main.go @@ -0,0 +1,161 @@ +package main + +import ( + "fmt" + "os" + "strconv" + "strings" + "time" + "unicode" + + "github.com/fatih/color" + "golang.org/x/crypto/ssh/terminal" +) + +var ( + inputBuffer = new(ColorString) + oldLineContent = new(ColorString) + oldWidth int + + debugInfo string +) + +func main() { + // enable raw mode + doItRaw() + defer cook() + + inputChannel := handleInput() + + running := true + + refreshInputBuffer(true) + + for running { + select { + case input := <-inputChannel: + if unicode.IsControl(rune(input)) { + if input == 127 && inputBuffer.Len() > 0 { // Backspace + str := inputBuffer.Raw() + inputBuffer = new(ColorString) + inputBuffer.Append(str[:len(str)-1]) + } else if input == 12 { // C-L + clear() + refreshInputBuffer(true) + } else if input == 13 { // Enter + cmd := inputBuffer.Raw() + if strings.TrimSpace(cmd) == "exit" { + running = false + break + } + inputBuffer = new(ColorString) + fmt.Print("\r\n") + handle(cmd) + refreshInputBuffer(true) + } else if input == 3 { // C-C + inputBuffer = new(ColorString) + } else if input == 17 { // C-Q + running = false + break + } + //fmt.Print(input, "\r\n") + break + } + inputBuffer.Append(string(input)) + case <-time.After(time.Second): + + } + + refreshInputBuffer(false) + } +} + +func refreshInputBuffer(force bool) { + ps := prompt() + lineContent := new(ColorString).AppendColorString(ps, inputBuffer) + + width, _, err := terminal.GetSize(int(os.Stdin.Fd())) + if err != nil { + panic(err) + } + if oldWidth == 0 { + oldWidth = width + } + + lines := oldLineContent.Len() / width + debugInfo = strconv.Itoa(lines) + + for i := 0; i <= lines; i++ { + fmt.Print("\r") + clearEOL() + + if i < lines { + goUp() + } + } + + fmt.Print(lineContent.String()) + oldLineContent = lineContent + oldWidth = width +} + +// func refreshInputBuffer(force bool) { +// width, _, err := terminal.GetSize(int(os.Stdin.Fd())) +// if err != nil { +// panic(err) +// } + +// clearEOL() +// debugInfo = fmt.Sprint(oldLineContent.Len(), width, oldLineContent.Len()/width) +// for i := 0; i < oldLineContent.Len()/width; i++ { +// goUp() +// } + +// ps := prompt() +// lineContent := new(ColorString).AppendColorString(ps, inputBuffer) +// if !force && lineContent.Equals(oldLineContent) { +// return +// } + +// diff := lineContent.Len() - oldLineContent.Len() +// if diff < 0 { +// diff = -diff +// } + +// for i := 0; i < diff; i++ { +// goLeft() +// } +// clearEOL() + +// fmt.Print("\r") +// fmt.Print(lineContent.String()) +// oldLineContent = lineContent +// } + +// func getTerminalSize() (width, height int, err error) { +// width, height, err = terminal.GetSize(int(os.Stdout.Fd())) +// if err != nil { +// return 0, 0, adverr.Wrap("Could not retrieve terminal size", err) +// } +// return +// } + +func print(str string, attrs ...color.Attribute) string { + return color.New(attrs...).Sprint(str) +} + +func host() string { + hostname, _ := os.Hostname() + return hostname +} + +func wd() string { + wd, _ := os.Getwd() + home := env("HOME") + return strings.Replace(wd, home, "~", 1) +} + +func env(key string) string { + value, _ := os.LookupEnv(key) + return value +} diff --git a/prompt.go b/prompt.go new file mode 100644 index 0000000..012c4ea --- /dev/null +++ b/prompt.go @@ -0,0 +1,87 @@ +package main + +import ( + "time" + + "github.com/fatih/color" +) + +var prompts = []func() *ColorString{ps1, ps2, ps3, ps4, ps5} + +func prompt() *ColorString { + // width, _, err := terminal.GetSize(int(os.Stdin.Fd())) + // if err != nil { + // return ">" + // } + + return ps1() + + // for _, pfunc := range prompts { + // prompt := pfunc() + // if prompt.Len()+inputBuffer.Len() < width { + // return prompt.String() + // } + // } +} + +func ps1() *ColorString { + p := new(ColorString) + p.Append(time.Now().Format("15:04:05"), color.FgRed) + p.Append(" • ") + + p.Append(debugInfo, color.FgMagenta) + if len(debugInfo) > 0 { + p.Append(" • ") + } + + p.Append(env("USER"), color.FgGreen) + p.Append(" • ") + p.Append(host(), color.FgYellow) + p.Append(" • ") + p.Append(wd(), color.FgBlue) + p.Append(" ❯", color.FgRed) + p.Append("❯", color.FgYellow) + p.Append("❯ ", color.FgGreen) + return p +} + +func ps2() *ColorString { + p := new(ColorString) + p.Append(env("USER"), color.FgGreen) + p.Append(" • ") + p.Append(host(), color.FgYellow) + p.Append(" • ") + p.Append(wd(), color.FgBlue) + p.Append(" ❯", color.FgRed) + p.Append("❯", color.FgYellow) + p.Append("❯ ", color.FgGreen) + return p +} + +func ps3() *ColorString { + p := new(ColorString) + p.Append(env("USER"), color.FgGreen) + p.Append(" • ") + p.Append(wd(), color.FgBlue) + p.Append(" ❯", color.FgRed) + p.Append("❯", color.FgYellow) + p.Append("❯ ", color.FgGreen) + return p +} + +func ps4() *ColorString { + p := new(ColorString) + p.Append(wd(), color.FgBlue) + p.Append(" ❯", color.FgRed) + p.Append("❯", color.FgYellow) + p.Append("❯ ", color.FgGreen) + return p +} + +func ps5() *ColorString { + p := new(ColorString) + p.Append(" ❯", color.FgRed) + p.Append("❯", color.FgYellow) + p.Append("❯ ", color.FgGreen) + return p +}