diff --git a/client/client.go b/client/client.go index 6ab7560..979096a 100644 --- a/client/client.go +++ b/client/client.go @@ -5,6 +5,7 @@ import ( "fmt" "io" + "github.com/altid/libs/fs" "github.com/lionkov/go9p/p" ) @@ -44,7 +45,7 @@ type runner interface { connect(int) error attach() error auth() error - ctl(int, ...string) (int, error) // Just call write at the end in nested types + ctl(int, ...string) (int, error) tabs() ([]byte, error) title() ([]byte, error) status() ([]byte, error) @@ -53,6 +54,7 @@ type runner interface { notifications() ([]byte, error) feed() (io.ReadCloser, error) document() ([]byte, error) + getCommands() ([]*fs.Command, error) } // NewClient returns a client ready to connect to addr @@ -79,6 +81,11 @@ func NewMockClient(addr string) *Client { } } +// GetCommands returns a list of available commands for the connected service +func (c Client) GetCommands() ([]*fs.Command, error) { + return c.run.getCommands() +} + // Document returns the contents of a document file on the host // if it exists, or an error func (c *Client) Document() ([]byte, error) { diff --git a/client/client_default.go b/client/client_default.go index 2791682..4f6ebed 100644 --- a/client/client_default.go +++ b/client/client_default.go @@ -5,6 +5,7 @@ import ( "io" "net" + "github.com/altid/libs/fs" "github.com/knieriem/g/go9p/user" "github.com/lionkov/go9p/p" "github.com/lionkov/go9p/p/clnt" @@ -125,6 +126,16 @@ func (c *client) notifications() ([]byte, error) { return c.clnt.Read(nfid, 0, p.MSIZE) } +func (c *client) getCommands() ([]*fs.Command, error) { + _, err := getNamedFile(c, "ctl") + if err != nil { + return nil, err + } + // Parse into Command struct + + return nil, nil +} + func (c *client) feed() (io.ReadCloser, error) { nfid := c.clnt.FidAlloc() diff --git a/client/client_mock.go b/client/client_mock.go index 94f06e4..92deae9 100644 --- a/client/client_mock.go +++ b/client/client_mock.go @@ -6,6 +6,7 @@ import ( "os" "time" + "github.com/altid/libs/fs" fuzz "github.com/google/gofuzz" ) @@ -39,6 +40,11 @@ func (c *mock) auth() error { return nil } +func (c *mock) getCommands() ([]*fs.Command, error) { + // TODO(halfwit): Mock up a general: list + return nil, nil +} + // We want to eventually create and track tabs internally to the library func (c *mock) ctl(cmd int, args ...string) (int, error) { data, err := runClientCtl(cmd, args...) diff --git a/client/client_test.go b/client/client_test.go index 63710ae..7f29e0c 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -53,7 +53,7 @@ func TestCommands(t *testing.T) { if _, e := mc.Input([]byte("Some text")); e != nil { t.Error(e) } - + if _, e := mc.Open("chicken"); e != nil { t.Error(e) } diff --git a/client/cmd/altid-cli/listen.go b/client/cmd/altid-cli/listen.go new file mode 100644 index 0000000..d554bd2 --- /dev/null +++ b/client/cmd/altid-cli/listen.go @@ -0,0 +1,160 @@ +package main + +import ( + "bufio" + "errors" + "io" + "os" + "strings" + "time" + + "github.com/altid/libs/client" +) + +type listener struct { + err chan error + data chan []byte + done chan struct{} + rd *bufio.Reader + c *client.Client +} + +func newListener(c *client.Client) *listener { + return &listener{ + rd: bufio.NewReader(os.Stdin), + err: make(chan error), + data: make(chan []byte), + done: make(chan struct{}), + c: c, + } +} + +func (l *listener) listen() { + if emitFeedData(l) != nil && emitDocumentData(l) != nil { + l.err <- errors.New("Unable to find feed or document for given buffer") + return + } + + for { + line, err := l.rd.ReadString('\n') + if err != nil && err != io.EOF { + l.err <- err + return + } + + if line == "/quit" { + l.done <- struct{}{} + return + } + + handle(l, strings.Fields(line)) + } +} + +func handle(l *listener, args []string) { + switch args[0] { + case "/help": + l.data <- []byte(usage) + case "/buffer": + sendCmd(l, l.c.Buffer, args[1]) + case "/open": + sendCmd(l, l.c.Open, args[1]) + case "/close": + sendCmd(l, l.c.Close, args[1]) + // TODO(halfwit): We want to track the current buffer + // and only send the `from` field internally + case "/link": + //sendCmd(l, l.c.Link, 2, args) + case "/tabs": + getData(l, l.c.Tabs) + case "/title": + getData(l, l.c.Title) + case "/status": + getData(l, l.c.Status) + case "/aside": + getData(l, l.c.Aside) + case "/notify": + getData(l, l.c.Notifications) + default: + otherMsg(l, args) + } +} + +func otherMsg(l *listener, args []string) { + if args[0][0] == '/' { + //cl.Ctl([]byte(line[1:])) + return + } + + line := strings.Join(args, " ") + l.c.Input([]byte(line)) +} + +func emitDocumentData(l *listener) error { + f, err := l.c.Document() + if err != nil { + return err + } + + if len(f) > 0 { + l.data <- f + } + + return nil +} + +func emitFeedData(l *listener) error { + f, err := l.c.Feed() + if err != nil { + return err + } + + go func() { + defer f.Close() + + for { + // Ensure your buffer is MSIZE + b := make([]byte, client.MSIZE) + + _, err := f.Read(b) + if err != nil { + l.err <- err + return + } + + if len(b) > 0 { + l.data <- b + } + } + }() + + return nil +} + +func sendCmd(l *listener, fn func(string) (int, error), args ...string) { + if len(args) != 1 { + l.err <- errBadArgs + return + } + + if _, err := fn(args[0]); err != nil { + l.err <- err + return + } + + time.Sleep(time.Millisecond * 200) + if emitFeedData(l) != nil && emitDocumentData(l) != nil { + l.err <- errors.New("Unable to find feed or document for given buffer") + } +} + +func getData(l *listener, fn func() ([]byte, error)) { + t, err := fn() + if err != nil { + l.err <- err + return + } + + l.data <- t + return +} diff --git a/client/cmd/altid-cli/main.go b/client/cmd/altid-cli/main.go index 36fb79d..7240c52 100644 --- a/client/cmd/altid-cli/main.go +++ b/client/cmd/altid-cli/main.go @@ -1,15 +1,10 @@ package main import ( - "bufio" "errors" "flag" - "fmt" - "io" "log" "os" - "strings" - "time" "github.com/altid/libs/client" ) @@ -59,151 +54,18 @@ func main() { log.Fatal(e) } - getDocument := func() { - f, err := cl.Document() - if err != nil { - log.Println("Unable to find a feed or document for this buffer") - return - } - - fmt.Printf("%s\n", f) - } - - getFeed := func() { - f, err := cl.Feed() - if err != nil { - getDocument() - return - } - - defer f.Close() - - for { - // Ensure your buffer is MSIZE - b := make([]byte, client.MSIZE) - - _, err := f.Read(b) - if err != nil && err != io.EOF { - log.Print(err) - return - } - - fmt.Printf("%s", b) - } - } - - go getFeed() - - rd := bufio.NewReader(os.Stdin) + l := newListener(cl) + go l.listen() + // Main loop for { - line, err := rd.ReadString('\n') - if err != nil && err != io.EOF { + select { + case <-l.done: break - } - - args := strings.Fields(line) - - switch args[0] { - case "/help": - fmt.Print(usage) - case "/quit": - os.Exit(0) - case "/buffer": - if len(args) != 2 { - log.Print(errBadArgs) - continue - } - if _, err := cl.Buffer(args[1]); err != nil { - log.Println(err) - continue - } - - time.Sleep(time.Millisecond * 200) - go getFeed() - case "/tabs": - t, err := cl.Tabs() - if err != nil { - log.Println(err) - continue - } - - fmt.Printf("%s", t) - case "/open": - if len(args) != 2 { - log.Print(errBadArgs) - continue - } - if _, err := cl.Open(args[1]); err != nil { - log.Println(err) - } - - time.Sleep(time.Millisecond * 200) - go getFeed() - case "/close": - if len(args) != 2 { - log.Print(errBadArgs) - continue - } - if _, err := cl.Close(args[1]); err != nil { - log.Println(err) - continue - } - - time.Sleep(time.Millisecond * 200) - go getFeed() - // TODO(halfwit): We want to track the current buffer - // and only send the `from` field internally - case "/link": - if len(args) != 3 { - log.Println(errBadArgs) - continue - } - if _, err := cl.Link(args[1], args[2]); err != nil { - log.Println(err) - continue - } - - time.Sleep(time.Millisecond * 200) - go getFeed() - case "/title": - t, err := cl.Title() - if err != nil { - log.Println(err) - continue - } - - fmt.Printf("%s", t) - case "/status": - t, err := cl.Status() - if err != nil { - log.Println(err) - continue - } - - fmt.Printf("%s", t) - case "/aside": - t, err := cl.Aside() - if err != nil { - log.Println(err) - continue - } - - fmt.Printf("%s", t) - case "/notify": - t, err := cl.Notifications() - if err != nil { - log.Println(err) - continue - } - - fmt.Printf("%s", t) - default: - if line[0] == '/' { - //cl.Ctl([]byte(line[1:])) - continue - } - cl.Input([]byte(line)) + case p := <-l.data: + os.Stdout.Write(p) + case e := <-l.err: + log.Println(e) } } } diff --git a/fs/commands.go b/fs/commands.go index 7922d17..5c5aff7 100644 --- a/fs/commands.go +++ b/fs/commands.go @@ -28,7 +28,7 @@ const ( const commandTemplate = `{{range .}} {{.Name}}{{if .Alias}}{{range .Alias}}|{{.}}{{end}}{{end}}{{if .Args}} {{range .Args}}{{.}} {{end}}{{end}}{{if .Description}} # {{.Description}}{{end}} {{end}}` -// Command represents an avaliable command to a service +// Command represents an available command to a service type Command struct { Name string Description string diff --git a/fs/commands_test.go b/fs/commands_test.go index 8d50b2b..5ea896f 100644 --- a/fs/commands_test.go +++ b/fs/commands_test.go @@ -19,7 +19,7 @@ func TestCommands(t *testing.T) { cmdlist = append(cmdlist, testMakeCmd("bar", []string{"<1>", "<2>"}, MediaGroup, []string{})) cmdlist = append(cmdlist, testMakeCmd("baz", []string{"<2>", "<1>"}, ActionGroup, []string{})) cmdlist = append(cmdlist, testMakeCmd("banana", []string{}, MediaGroup, []string{})) - cmdlist = append(cmdlist, testMakeCmd("nocomm", []string{}, ActionGroup, []string{"yacomm"})) + cmdlist = append(cmdlist, testMakeCmd("nocomm", []string{""}, ActionGroup, []string{"yacomm", "maybecomm"})) if e := c.SetCommands(cmdlist...); e != nil { t.Error(e) diff --git a/fs/ctl.go b/fs/ctl.go index bc8fe1c..21b3f0f 100644 --- a/fs/ctl.go +++ b/fs/ctl.go @@ -19,7 +19,7 @@ import ( // If a client attempts to write an invalid control message, it will return a generic error // When Open is called, a file will be created with a path of `mountpoint/msg/document (or feed)`, containing initially a file named what you've set doctype to.. Calls to open are expected to populate that file, as well as create any supplementary files needed, such as title, aside, status, input, etc // When Link is called, the content of the current buffer is expected to change, and the name of the current tab will be removed, replaced with msg -// The main document or feed file is also symlinked into the given log directory, under service/msgs, so for example, an expensive parse would only have to be completed once for a given request, even across seperate runs; or a chat log could have history from previous sessions accessible. +// The main document or feed file is also symlinked into the given log directory, under service/msgs, so for example, an expensive parse would only have to be completed once for a given request, even across separate runs; or a chat log could have history from previous sessions accessible. // The message provided to all three functions is all of the message, less 'open', 'join', 'close', or 'part'. type Controller interface { Open(c *Control, msg string) error @@ -83,31 +83,31 @@ const ( //TODO(halfiwt) i18n var defaultCommands = []*Command{ - &Command{ + { Name: "open", Args: []string{""}, Heading: DefaultGroup, Description: "Open and change buffers to a given service", }, - &Command{ + { Name: "close", Args: []string{""}, Heading: DefaultGroup, Description: "Close a buffer and return to the last opened previously", }, - &Command{ + { Name: "buffer", Args: []string{""}, Heading: DefaultGroup, Description: "Change to the named buffer", }, - &Command{ + { Name: "link", Args: []string{"", ""}, Heading: DefaultGroup, Description: "Overwrite the current buffer with , switching to from after. This destroys ", }, - &Command{ + { Name: "quit", Args: []string{}, Heading: DefaultGroup, @@ -255,7 +255,7 @@ func (c *Control) Listen() error { return c.run.listen() } -// Start is like listen, but occurs in a seperate go routine, returning flow to the calling process once the ctl file is instantiated. +// Start is like listen, but occurs in a separate go routine, returning flow to the calling process once the ctl file is instantiated. // This provides a context.Context that can be used for cancellations func (c *Control) Start() (context.Context, error) { go sigwatch(c) diff --git a/fs/ctl_default.go b/fs/ctl_default.go index 2d6d632..541e78f 100644 --- a/fs/ctl_default.go +++ b/fs/ctl_default.go @@ -25,6 +25,14 @@ type control struct { done chan struct{} } +type controlrunner struct { + ctx context.Context + scanner *bufio.Scanner + done chan struct{} + req chan string + cancel context.CancelFunc +} + func (c *control) event(eventmsg string) error { file := path.Join(c.rundir, "event") if _, err := os.Stat(path.Dir(file)); os.IsNotExist(err) { @@ -135,51 +143,31 @@ func (c *control) hasBuffer(name, doctype string) bool { return true } -func (c *control) listen() error { - if e := os.MkdirAll(c.rundir, 0755); e != nil { - return e - } - cfile := path.Join(c.rundir, "ctl") +func (c *control) listen() error { + ctx, cancel := context.WithCancel(context.Background()) - ctl, err := os.Create(cfile) + cr, err := newControlRunner(ctx, cancel, c) if err != nil { return err } - if e := printCtlFile(c.cmdlist, ctl); e != nil { - return e - } - - ctl.Close() + cr.listen() - b, err := ioutil.ReadFile(cfile) - if err != nil { - return err - } + return nil +} - fmt.Printf("%s\n", b) +func (c *control) start() (context.Context, error) { + ctx, cancel := context.WithCancel(context.Background()) - r, err := newReader(cfile) + cr, err := newControlRunner(ctx, cancel, c) if err != nil { - return err + return nil, err } - c.event(cfile) - scanner := bufio.NewScanner(r) - - for scanner.Scan() { - line := scanner.Text() - if line == "quit" { - close(c.done) - break - } + go cr.listen() - c.req <- line - } - - close(c.req) - return nil + return ctx, nil } func (c *control) remove(buffer, filename string) error { @@ -193,51 +181,6 @@ func (c *control) remove(buffer, filename string) error { return os.Remove(doc) } -func (c *control) start() (context.Context, error) { - if e := os.MkdirAll(c.rundir, 0755); e != nil { - return nil, e - } - - cfile := path.Join(c.rundir, "ctl") - c.event(cfile) - - ctl, err := os.Create(cfile) - if err != nil { - return nil, err - } - - if e := printCtlFile(c.cmdlist, ctl); e != nil { - return nil, e - } - - ctl.Close() - - r, err := newReader(cfile) - if err != nil { - return nil, err - } - - ctx, cancel := context.WithCancel(context.Background()) - - go func() { - defer close(c.req) - scanner := bufio.NewScanner(r) - - for scanner.Scan() { - line := scanner.Text() - if line == "quit" { - cancel() - close(c.done) - break - } - - c.req <- line - } - }() - - return ctx, nil -} - func (c *control) notification(buff, from, msg string) error { nfile := path.Join(c.rundir, buff, "notification") if _, e := os.Stat(path.Dir(nfile)); os.IsNotExist(e) { @@ -348,3 +291,58 @@ func (c *control) imageWriter(buffer, resource string) (*WriteCloser, error) { os.MkdirAll(path.Dir(path.Join(c.rundir, buffer, "images", resource)), 0755) return c.fileWriter(buffer, path.Join("images", resource)) } + +func newControlRunner(ctx context.Context, cancel context.CancelFunc, c *control) (*controlrunner, error) { + if e := os.MkdirAll(c.rundir, 0755); e != nil { + return nil, e + } + + cfile := path.Join(c.rundir, "ctl") + c.event(cfile) + + ctl, err := os.Create(cfile) + if err != nil { + return nil, err + } + + if e := printCtlFile(c.cmdlist, ctl); e != nil { + return nil, e + } + + ctl.Close() + + r, err := newReader(cfile) + if err != nil { + return nil, err + } + + cr := &controlrunner{ + scanner: bufio.NewScanner(r), + req: c.req, + ctx: ctx, + cancel: cancel, + done: c.done, + } + + return cr, nil +} + +func (c *controlrunner) listen() { + defer close(c.req) + + for c.scanner.Scan() { + select { + case <-c.ctx.Done(): + break + default: + line := c.scanner.Text() + if line == "quit" { + c.cancel() + close(c.done) + break + } + + c.req <- line + } + } +} diff --git a/fs/ctl_test.go b/fs/ctl_test.go index 95906f9..45c9074 100644 --- a/fs/ctl_test.go +++ b/fs/ctl_test.go @@ -67,7 +67,7 @@ func TestWriters(t *testing.T) { go func() { // `reqs <- "open foo"` is a race condition, but on a real client there will always // be an Open called before MainWriter (generally you call MainWriter in your client's Open method); - // So we explicitely call c.CreateBuffer to avoid in the mock client tests + // So we explicitly call c.CreateBuffer to avoid in the mock client tests c.CreateBuffer("foo", "feed") mw, err := c.MainWriter("foo", "feed") if err != nil { diff --git a/fs/input.go b/fs/input.go index 398cb99..e1a608f 100644 --- a/fs/input.go +++ b/fs/input.go @@ -126,7 +126,7 @@ func (i *Input) StartContext(ctx context.Context) { i.ctx = ctx go func() { - for msg := range inputs { + for msg := range inputs { l := markup.NewLexer(msg) if e := i.h.Handle(i.buff, l); e != nil { errors <- e diff --git a/fs/wc.go b/fs/wc.go index d788a4b..0fefb8d 100644 --- a/fs/wc.go +++ b/fs/wc.go @@ -13,7 +13,7 @@ func (w *WriteCloser) Write(b []byte) (n int, err error) { return w.fp.Write(b) } -// Close - A Closer which sends an event +// Close - A Closer which sends an event func (w *WriteCloser) Close() error { w.c.event(w.buffer) return w.fp.Close() diff --git a/go.mod b/go.mod index 04ca4ec..2997f78 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.14 require ( 9fans.net/go v0.0.2 + github.com/fzipp/gocyclo v0.0.0-20150627053110-6acd4345c835 // indirect github.com/google/gofuzz v1.1.0 github.com/knieriem/g v0.1.2 github.com/lionkov/go9p v0.0.0-20190125202718-b4200817c487 diff --git a/go.sum b/go.sum index ebe2c9d..aa81cf3 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ 9fans.net/go v0.0.2 h1:RYM6lWITV8oADrwLfdzxmt8ucfW6UtP9v1jg4qAbqts= 9fans.net/go v0.0.2/go.mod h1:lfPdxjq9v8pVQXUMBCx5EO5oLXWQFlKRQgs1kEkjoIM= +github.com/fzipp/gocyclo v0.0.0-20150627053110-6acd4345c835 h1:roDmqJ4Qes7hrDOsWsMCce0vQHz3xiMPjJ9m4c2eeNs= +github.com/fzipp/gocyclo v0.0.0-20150627053110-6acd4345c835/go.mod h1:BjL/N0+C+j9uNX+1xcNuM9vdSIcXCZrQZUYbXOFbgN8= github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/knieriem/g v0.1.2 h1:rsuzp2xAM3d3zkIkDPS9gV7G/60i30439bcNc3Nicsw= diff --git a/html/html.go b/html/html.go index 782d734..cba5d0a 100644 --- a/html/html.go +++ b/html/html.go @@ -84,7 +84,7 @@ func (c *HTMLCleaner) Parse(r io.ReadCloser) error { case tags[atom.H6]: fmt.Fprintf(c.w, "###### ") } - url, msg := parseUrl(z, t) + url, msg := parseURL(z, t) fmt.Fprintf(c.w, "[%s](%s)", url, msg) continue } @@ -165,9 +165,11 @@ func endToken(t atom.Atom) string { func parseToken(t html.Token, m map[atom.Atom]bool) string { // NOTE(halfwit): This is rather messy right now, and will need a revisit var dst bytes.Buffer + if m[atom.Script] || m[atom.Head] { return "" } + switch { case m[atom.H1]: dst.WriteString("# ") @@ -195,7 +197,10 @@ func parseToken(t html.Token, m map[atom.Atom]bool) string { case m[atom.Li]: dst.WriteString(" - ") } + d := t.Data + + // If all we had is whitespace, don't return anything if strings.TrimSpace(d) == "" { return "" } @@ -206,7 +211,7 @@ func parseToken(t html.Token, m map[atom.Atom]bool) string { // TODO: Give back triple, containing link, url, image // Switch on image == "" -func parseUrl(z *html.Tokenizer, t html.Token) (link, url string) { +func parseURL(z *html.Tokenizer, t html.Token) (link, url string) { for _, attr := range t.Attr { if attr.Key == "href" { url = attr.Val @@ -264,7 +269,7 @@ func parseNav(z *html.Tokenizer, t html.Token) chan *markup.Url { if t.DataAtom != atom.A { continue } - link, url := parseUrl(z, t) + link, url := parseURL(z, t) m <- &markup.Url{ Link: []byte(link), Msg: []byte(url), diff --git a/markup/lexer.go b/markup/lexer.go index c1c4ea1..bcc1cad 100644 --- a/markup/lexer.go +++ b/markup/lexer.go @@ -49,7 +49,7 @@ func NewLexer(src []byte) *Lexer { func NewStringLexer(src string) *Lexer { return &Lexer{ - src: []byte(src), + src: []byte(src), items: make(chan Item, 2), state: lexText, } @@ -602,6 +602,6 @@ func (l *Lexer) acceptRun(valid string) { if strings.IndexByte(valid, l.nextChar()) < 0 { l.backup() return - } + } } } diff --git a/markup/text.go b/markup/text.go index db1bbb5..5c1f658 100644 --- a/markup/text.go +++ b/markup/text.go @@ -34,7 +34,7 @@ var ( ) // Color represents a color markdown element -// Valid values for code are any [markup constants], or color strings in hexidecimal form. +// Valid values for code are any [markup constants], or color strings in hexadecimal form. // #000000 to #FFFFFF, as well as #000 to #FFF. No alpha channel support currently exists. type Color struct { code string diff --git a/markup/text_test.go b/markup/text_test.go index d600c91..5f0d75b 100644 --- a/markup/text_test.go +++ b/markup/text_test.go @@ -87,4 +87,4 @@ func TestEscapeString(t *testing.T) { if markup.EscapeString(c) != "this \\*is\\* \\~my \\_test\\_ string\\~ to \\-see\\-" { t.Error("parsing error in EscapeString") } -} \ No newline at end of file +}