diff --git a/README.md b/README.md index 9db7a90..35f3a68 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,9 @@ https://github.com/as/a/wiki/Alterations https://github.com/as/a/issues # hints -To reshape the windows and columns, click on the invisible 10x10px sizer that I haven't rendered yet with the middle mouse button. Hold the button down and move the window to the location. Release the button. +To reshape the windows and columns, click on the invisible 10x10px sizer that I haven't rendered yet with the left mouse button. +Hold the button down and move the window to the location. +Release the button. # edit - 80% of the sam command language is implemented. @@ -83,10 +85,6 @@ Edit ,x,the standard editor is any editor,x,any editor,c,ed, - Looking (right click) in the main tag finds the result in all open windows ![paint](jump.png) -# purpose -- ACME SAC doesn't run on my computer -- The solution is to create a text editor from scratch then - # future - Fixing the bugs - Cleaning the code up diff --git a/a.go b/a.go index 83b4b50..06ab7d0 100644 --- a/a.go +++ b/a.go @@ -1,31 +1 @@ package main - -import ( - "github.com/as/edit" - "github.com/as/frame" -) - -func (g *Grid) acolor(e edit.File) { - // TODO(as): O(n*m) -> O(1) - if t := g.FindName(e.Name); t != nil { - if t.Body == nil { - return - } - fr := t.Body.Frame - p0, p1 := e.Q0, e.Q1 - p0 -= clamp(p0-t.Body.Origin(), 0, fr.Len()) - p1 -= clamp(p1-t.Body.Origin(), 0, fr.Len()) - fr.Recolor(fr.PointOf(p0), p0, p1, frame.Mono.Palette) - fr.Mark() - } -} - -func clamp(v, l, h int64) int64 { - if v < l { - return l - } - if v > h { - return h - } - return v -} diff --git a/active.go b/active.go index aad92eb..3b5e934 100644 --- a/active.go +++ b/active.go @@ -4,15 +4,21 @@ import ( "image" "github.com/as/ui/tag" - "github.com/as/ui/win" ) var ( actCol *Col actTag *tag.Tag - act *win.Win + act tag.Window ) +func actinit(g *Grid) { + // This in particular needs to go + actCol = g.List[0].(*Col) + actTag = actCol.List[0].(*tag.Tag) + act = actTag.Body +} + func active2(pt image.Point, list ...Plane) (x Plane) { for i, w := range list { if w == nil { @@ -26,36 +32,29 @@ func active2(pt image.Point, list ...Plane) (x Plane) { return nil } -// Put -func active(pt image.Point, act Plane, list ...Plane) (x Plane) { - if tag.Buttonsdown != 0 { - return act +func activelabel(pt image.Point, t *tag.Tag) bool { + if t.Win == nil { + panic("FLAG: label is nil") } - if act != nil { - list = append([]Plane{act}, list...) - } - for i, w := range list { - if w == nil { - continue - } - r := w.Loc() - if pt.In(r) { - return list[i] - } + if pt.In(t.Win.Loc()) { + actTag = t + act = t.Win + return true } - return act + return false } func activate(pt image.Point, w Plane) { - if tag.Buttonsdown != 0 { - return - } switch w := w.(type) { case *Grid: + if activelabel(pt, w.Tag) { + return + } x := active2(pt, w.List...) switch x := x.(type) { case *tag.Tag: - actCol = w.Col + panic("tag not allowed in column anymore") + //actCol = w.Col actTag = x act = x.Win case *Col: @@ -65,19 +64,17 @@ func activate(pt image.Point, w Plane) { } case *Col: actCol = w - x := active2(pt, w.List...) - if eq(x, w.List[0]) { - actTag = x.(*tag.Tag) - act = x.(*tag.Tag).Win - } else { - activate(pt, x) + if activelabel(pt, w.Tag) { + return } + x := active2(pt, w.List...) + activate(pt, x) case *tag.Tag: actTag = w if w.Body != nil { activate(pt, active2(pt, w.Body, w.Win)) } - case *win.Win: + case tag.Window: act = w } } diff --git a/args.go b/args.go new file mode 100644 index 0000000..6f2b7a7 --- /dev/null +++ b/args.go @@ -0,0 +1,13 @@ +package main + +import "flag" + +func argparse() (list []string) { + if len(flag.Args()) > 0 { + list = append(list, flag.Args()...) + } else { + list = append(list, "guide") + list = append(list, ".") + } + return +} diff --git a/assert.go b/assert.go new file mode 100644 index 0000000..fa56547 --- /dev/null +++ b/assert.go @@ -0,0 +1,61 @@ +package main + +import ( + "image" + "log" + + "github.com/as/ui/tag" +) + +const debug = 1 ^ 1 + +func assert(where string, g *Grid) { + if debug == 0 { + return + } + log.Printf("%s: grid %s\n", where, g.Loc()) + for i, c := range g.List { + for j, c2 := range g.List { + if c == c2 { + continue + } + if c.Loc().Intersect(c2.Loc()) != image.ZR { + log.Printf("%s: col %v %s intersects col %v %s\n", where, i, c.Loc(), j, c2.Loc()) + panic("suicide") + } + } + } + if g.Tag.Vis != tag.VisTag { + log.Printf("%v\n", g.Tag.Vis) + // panic("grid tag not vistag") // Put + } + for i, c := range g.List { + if c.(*Col).Tag.Vis != tag.VisTag { + log.Printf("number %d = %v\n", i, g.Tag.Vis) + //panic("col tag not vistag") + } + if !c.Loc().In(g.Loc()) { + log.Printf("%s: col %v %s not in grid %s\n", where, i, c.Loc(), g.Loc()) + // panic("suicide") + } + log.Printf("%s: col %v %s is good\n", where, i, c.Loc()) + { + c, _ := c.(*Col) + if c == nil { + continue + } + for j, t := range c.List { + if !t.Loc().In(c.Loc()) { + log.Printf("%s: tag %v %s not in col %v %s\n", where, j, t.Loc(), i, c.Loc()) + //panic("suicide") + } + if !t.Loc().In(g.Loc()) { + log.Printf("%s: tag %v %s not in grid %v %s\n", where, j, t.Loc(), i, g.Loc()) + //panic("suicide") + + } + log.Printf("%s: tag %v %s is good\n", where, j, t.Loc()) + } + } + } +} diff --git a/border.go b/border.go new file mode 100644 index 0000000..c508cb4 --- /dev/null +++ b/border.go @@ -0,0 +1,69 @@ +package main + +import ( + mus "github.com/as/text/mouse" + "github.com/as/ui/col" + "github.com/as/ui/win" + "golang.org/x/mobile/event/mouse" +) + +func borderHit(e mouse.Event) bool { + pt := p(e) + return inSizer(pt) || inScroll(pt) +} + +func procBorderHit(e mouse.Event) { + apt := p(e) + e = rel(e, act) + pt := p(e) + + switch { + case inSizer(pt): + if HasButton(1, down) { + if !apt.In(g.Area()) { + for down != 0 { + g.Move(apt) + g.Refresh() + col.Fill(g) + apt = p(readmouse(<-D.Mouse)) + } + } else if canopy(apt) { + dragCol(g, actCol, e, D.Mouse) + } else { + dragTag(actCol, actTag, e, D.Mouse) + } + break + } + switch down { + case Button(2): + case Button(3): + // actCol.PrintList() + actCol.RollUp(actCol.ID(actTag), act.Loc().Min.Y) + // actCol.PrintList() + moveMouse(act.Loc().Min) + } + for down != 0 { + readmouse(<-D.Mouse) + } + case inScroll(pt): + switch down { + case Button(1): + scroll(act, mus.ScrollEvent{Dy: 10, Event: e}) + case Button(2): + w, _ := act.(*win.Win) + if w == nil { + break + } + w.Clicksb(pt, 0) + repaint() + for HasButton(2, down) { + w.Clicksb(p(rel(readmouse(<-D.Mouse), w)), 0) + repaint() + } + case Button(3): + scroll(act, mus.ScrollEvent{Dy: -10, Event: e}) + } + default: + logf("unknown border action at pt: %s", pt) + } +} diff --git a/cmd.go b/cmd.go new file mode 100644 index 0000000..bd9f112 --- /dev/null +++ b/cmd.go @@ -0,0 +1,252 @@ +package main + +import ( + "bytes" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "strings" + "sync" + + "github.com/as/edit" + "github.com/as/event" + "github.com/as/path" + "github.com/as/text" + "github.com/as/ui/tag" + "github.com/as/ui/win" +) + +var null, _ = os.Open(os.DevNull) + +func editcmd(ed interface{}, origin, cmd string) { + prog, err := edit.Compile(cmd, &edit.Options{Sender: nil, Origin: origin}) + if err != nil { + logf("editcmd: %s", err) + return + } + runeditcmd(prog, ed) + { + ed, _ := ed.(text.Editor) + if ed != nil { + ajump2(ed, false) + } + } +} + +func runeditcmd(prog *edit.Command, ed interface{}) { + switch ed := ed.(type) { + case *win.Win: + if ed == actTag.Win { + ed = actTag.Body.(*win.Win) + } + prog.Run(ed) + case *tag.Tag: + prog.Run(ed.Body) + case *Grid: + for _, ed := range ed.List { + runeditcmd(prog, ed) + } + case *Col: + for _, ed := range ed.List { + runeditcmd(prog, ed) + } + case text.Editor: + prog.Run(ed) + case interface{}: + logf("dont know what %T is", ed) + } +} + +func acmd(e event.Cmd) { + s := string(e.P) + switch s { + case "Put": + actTag.Put() + repaint() + case "Get": + actTag.Get(actTag.FileName()) + repaint() + case "New": + newtag := New(actCol, "", "") + moveMouse(newtag.Loc().Min) + case "Newcol": + moveMouse(NewColParams(g, "").Loc().Min) + case "Del": + Del(actCol, actCol.ID(actTag)) + case "Sort": + logf("Sort: TODO") + case "Delcol": + Delcol(g, g.ID(actCol)) + case "Exit": + logf("Exit: TODO") + default: + if len(e.To) == 0 { + logf("cmd has no destination: %q", s) + } + abs := AbsOf(e.Basedir, e.Name) + if strings.HasPrefix(s, "Edit ") { + s = s[5:] + editcmd(e.To[0], abs, s) + break + prog, err := edit.Compile(s, &edit.Options{Sender: nil, Origin: abs}) + if err != nil { + logf(err.Error()) + return + } + ed := text.Editor(e.To[0]) + if e.To[0] == actTag.Win { + ed = actTag.Body + } + prog.Run(ed) + ajump2(ed, false) + } else if strings.HasPrefix(s, "Install ") { + s = s[8:] + g.Install(actTag, s) + } else { + x := strings.Fields(s) + if len(x) < 1 { + logf("empty command") + return + } + cmdexec(nil, path.DirOf(abs), s) + } + } +} + +func cmdexec(input text.Editor, dir string, argv string) { + x := strings.Fields(argv) + if len(x) == 0 { + logf("|: nothing on rhs") + return + } + n := x[0] + var a []string + if len(x) > 1 { + a = x[1:] + } + + cmd := exec.Command(n, a...) + cmd.Dir = dir + var fd0 io.WriteCloser + fd1, _ := cmd.StdoutPipe() + fd2, _ := cmd.StderrPipe() + fd0, _ = cmd.StdinPipe() + + fd0.Close() + var wg sync.WaitGroup + donec := make(chan bool) + outc := make(chan []byte) + errc := make(chan []byte) + wg.Add(2) + go func() { + defer wg.Done() + b := make([]byte, 65536) + for { + select { + case <-donec: + return + default: + n, err := fd1.Read(b) + if n > 0 { + outc <- append([]byte{}, b[:n]...) + } + if err != nil { + if err != io.EOF { + eprint(err) + } + return + } + } + } + }() + + go func() { + defer wg.Done() + b := make([]byte, 65536) + for { + select { + case <-donec: + return + default: + n, err := fd2.Read(b) + if n > 0 { + errc <- append([]byte{}, b[:n]...) + } + if err != nil { + if err != io.EOF { + eprint(err) + } + return + } + } + } + }() + err := cmd.Start() + if err != nil { + logf("exec: %s: %s", argv, err) + close(donec) + return + } + + var ( + q0, q1 int64 + f text.Editor + ) + lazyinit := func() { + to := g.afinderr(dir, cmdlabel(n, dir)) + f = to.Body + q0, q1 := f.Dot() + f.Delete(q0, q1) + q1 = q0 + } + + go func() { + var stdin io.Reader + stdin = null + if input != nil { + stdin = bytes.NewReader(append([]byte{}, input.Bytes()[q0:q1]...)) + } + _, err := io.Copy(fd0, stdin) + if err != nil { + eprint(err) + return + } + cmd.Wait() + close(donec) + }() + go func() { + select { + case p := <-outc: + lazyinit() + f.Insert(p, q1) + q1 += int64(len(p)) + case p := <-errc: + lazyinit() + f.Insert(p, q1) + q1 += int64(len(p)) + case <-donec: + return + } + repaint() + for { + select { + case p := <-outc: + f.Insert(p, q1) + q1 += int64(len(p)) + case p := <-errc: + f.Insert(p, q1) + q1 += int64(len(p)) + case <-donec: + return + } + repaint() + } + }() +} + +func cmdlabel(name, dir string) (label string) { + return fmt.Sprintf("%s%c-%s", dir, filepath.Separator, name) + +} diff --git a/col.go b/col.go index a6568e4..08134e7 100644 --- a/col.go +++ b/col.go @@ -1,50 +1,78 @@ package main import ( - "fmt" "image" "io" - "sync" "github.com/as/font" - "github.com/as/frame" - "github.com/as/shiny/screen" "github.com/as/ui" + "github.com/as/ui/col" "github.com/as/ui/tag" + "github.com/as/ui/win" ) -type Col struct { - dev *ui.Dev - ft font.Face - sp image.Point - size image.Point - Tag *tag.Tag - tdy int - List []Plane +type Col = col.Col + +func phi(r image.Rectangle) image.Point { + size := r.Size() + size = size.Sub(size.Div(3)) + return r.Min.Add(size) } -func New(co *Col, basedir, name string, sizerFunc ...func(int) int) (w Plane) { - last := co.List[len(co.List)-1] - last.Loc() - t := tag.New(co.dev, co.sp, image.Pt(co.size.X, co.tdy*2), nil) +func underText(p Plane) image.Point { + pt := phi(p.Loc()) + t, _ := p.(*tag.Tag) + if t == nil { + return pt + } + w, _ := t.Body.(*win.Win) + if w == nil || !w.Graphical() { + return pt + } + if w.Frame.Full() { + return pt + } + return w.Area().Min.Add(w.PointOf(w.Frame.Len())) +} +// New creates opens a names resource as a tagged window in column c +func New(c *Col, basedir, name string) (w Plane) { + t := tag.New(c.Dev(), TagConfig) t.Open(basedir, name) t.Insert([]byte(" [Edit ,x]"), t.Len()) - lsize := sizeof(last.Loc()) - - fn := SizeThirdOf - if len(sizerFunc) != 0 { - fn = sizerFunc[0] + r := c.Area() + if c.Len() > 0 { + r.Min = underText(c.List[len(c.List)-1]) + } else { + r.Min = phi(r) } - lsize.Y = fn(lsize.Y) - last.Resize(lsize) - co.attach(t, len(co.List)) - co.fill() + col.Attach(c, t, r.Min) return t } +func NewCol(dev ui.Dev, ft font.Face, sp, size image.Point, files ...string) *Col { + c := col.New(dev, ColConfig) + c.Tag.InsertString("New Delcol Sort |", 0) + c.Move(sp) + c.Resize(size) + for _, name := range files { + New(c, "", name) + } + return c +} + +func NewColParams(g *Grid, filenames ...string) *Col { + r := g.Area() + if len(g.List) != 0 { + r = g.List[len(g.List)-1].Loc() + } + c := NewCol(g.Dev(), g.Face(), r.Min, r.Size(), filenames...) + col.Attach(g, c, phi(r)) + return c +} + func Delcol(g *Grid, id int) { - co := g.detach(id) + co := col.Detach(g, id) x := co.Loc().Min.X y := co.Loc().Min.Y for ; id < len(g.List); id++ { @@ -52,10 +80,11 @@ func Delcol(g *Grid, id int) { g.List[id].Move(image.Pt(x, y)) x = x2 } - g.fill() + col.Fill(g) } + func Del(co *Col, id int) { - w := co.detach(id) + w := co.Detach(id) y := w.Loc().Min.Y x := co.Loc().Min.X w.(io.Closer).Close() @@ -64,316 +93,5 @@ func Del(co *Col, id int) { co.List[id].Move(image.Pt(x, y)) y = y2 } - co.fill() -} - -func NewCol(dev *ui.Dev, ft font.Face, sp, size image.Point, files ...string) *Col { - N := len(files) - tdy := ft.Dy() + ft.Dy()/2 - tagpad := image.Pt(pad.X, 3) - conf := &tag.Config{ - Margin: tagpad, - Facer: font.NewFace, - FaceHeight: ft.Height(), - Color: [3]frame.Color{ - 0: frame.ATag1, - }, - } - T := tag.New(dev, sp, image.Pt(size.X, tdy), conf) - //T.Open(path.NewPath("")) - T.Win.InsertString("New Delcol Sort", 0) - col := &Col{dev: dev, sp: sp, size: size, ft: ft, Tag: T, tdy: tdy, List: make([]Plane, len(files))} - size.Y -= tdy - sp.Y += tdy - dy := image.Pt(size.X, size.Y/N) - for i, v := range files { - conf.Margin = pad - t := tag.New(dev, sp, dy, conf) - t.Get(v) - t.Insert([]byte(" [Edit ,x]"), t.Len()) - col.List[i] = t - sp.Y += dy.Y - } - col.List = append([]Plane{T}, col.List...) - return col -} - -func NewCol2(g *Grid, filenames ...string) (w Plane) { - x0 := g.List[0].Loc().Min.X - y0 := g.List[0].Loc().Dy() - x1 := g.sp.X + g.size.X - y1 := g.sp.X + g.size.Y - y0 - if len(g.List) > 1 { - last := g.List[len(g.List)-1] - last.Resize(image.Pt(last.Loc().Dx()/2, last.Loc().Dy())) - x0 = last.Loc().Max.X - x1 = x0 + last.Loc().Dx()/2 - } - sp := image.Pt(x0, y0) - size := image.Pt(x1-x0, y1-y0) - col := NewCol(g.dev, g.ft, sp, size, filenames...) - g.attach(col, len(g.List)) - g.fill() - return col -} - -func (co *Col) Attach(src Plane, y int) { - did := co.IDPoint(image.Pt(co.sp.X, y)) - if did == 0 || did >= len(co.List) { - return - } - d := co.List[did] - y -= d.Loc().Min.Y - x := sizeof(d.Loc()).X - d.Resize(image.Pt(x, y)) - co.attach(src, did+1) - co.fill() -} - -func (co *Col) Close() error { - for _, t := range co.List { - if t == nil { - continue - } - if t, ok := t.(io.Closer); ok { - t.Close() - } - } - co.List = nil - return nil -} - -func (col *Col) PrintList() { - for i, v := range col.List { - fmt.Printf("%d: %#v\n", i, v) - } -} - -func (co *Col) FindName(name string) *tag.Tag { - for _, v := range co.List[1:] { - switch v := v.(type) { - case *Col: - t := v.FindName(name) - if t != nil { - return t - } - case *tag.Tag: - if v.FileName() == name { - return v - } - - } - } - return nil -} - -func (co *Col) IDPoint(pt image.Point) (id int) { - for id = 0; id < len(co.List); id++ { - if pt.In(co.List[id].Loc()) { - break - } - } - return id -} -func (co *Col) ID(w Plane) (id int) { - for id = 0; id < len(co.List); id++ { - if eq(w, co.List[id]) { - break - } - } - return id -} - -func (co *Col) Loc() image.Rectangle { - if co == nil { - return image.ZR - } - return image.Rectangle{co.sp, co.sp.Add(co.size)} -} - -func (co *Col) Move(sp image.Point) { - co.sp.X = sp.X - for _, t := range co.List { - sp := image.Pt(sp.X, t.Loc().Min.Y) - t.Move(sp) - } - co.fill() -} - -func (col *Col) Refresh() { - for _, v := range col.List { - v.Refresh() - } -} - -func (co *Col) Resize(size image.Point) { - co.size = size - co.fill() -} - -func (co *Col) RollUp(id int, dy int) { - if id <= 0 || id >= len(co.List) { - return - } - for x := 2; x <= id; x++ { - a := co.List[x-1].Loc() - dy = a.Min.Y + tagHeight - co.MoveWin(x, dy) - } - co.MoveWin(id, dy) -} -func (co *Col) badID(id int) bool { - return id <= 0 -} -func (co *Col) bestGrowth(id int, dy int) int { - if co.badID(id) || co.badID(id-1) { - return dy - } - nclicks := co.List[id-1].Loc().Dy() / dy - if nclicks < 3 { - return dy - } - if nclicks < 5 { - return dy * 2 - } - if nclicks < 8 { - return dy * 3 - } - return dy * 4 -} -func (co *Col) Grow(id int, dy int) { - a, b := id-1, id - if co.badID(a) || co.badID(b) { - return - } - ra, rb := co.List[a].Loc(), co.List[b].Loc() - ra.Max.Y -= dy - if dy := ra.Dy() - tagHeight; dy < 0 { - co.Grow(a, -dy) - } - fmt.Printf("min: %d, dy: %d, min-dy: %d\n", rb.Min.Y, dy, rb.Min.Y-dy) - co.MoveWin(b, rb.Min.Y-dy) -} - -/* -func (co *Col) Show(id int, dy int){ - if co.badID(id){ - return - } - r := co.List[id].Loc() - if r.Dy() > dy{ - return - } -} -*/ - -func (co *Col) RollDown(id int, dy int) { - /* - if id >= len(co.List) { - return - } - r := co.List[x].Loc() - if r.Min.Y+dy - a := r.Min.Y - b := a+co.List[x].Loc().Min.Y - - x:=id - a := co.List[x].Loc() - for x+1 < len(co.List){ - b := co.List[x+1].Loc() - if extra := a.Min.Y+tagHeight+dy-b.Min.Y; extra > 0{ - co.MoveWin(x, a.Min.Y+extra) - } - } - if a.Min.Y+dy > co.Loc().Max.Y{ - dy = co.Loc().Max.Y - tagHeight - } - co.MoveWin(id, a.Min.Y+dy) - */ -} -func (co *Col) Upload(wind screen.Window) { - type Uploader interface { - Upload(screen.Window) - // Dirty() bool - } - for _, t := range co.List { - if t, ok := t.(Uploader); ok { - //if co.Dirty(){ - t.Upload(wind) - //} - } - } -} - -func (co *Col) MoveWin(id int, y int) { - if id == 0 || id >= len(co.List) { - return - } - maxy := co.List[len(co.List)-1].Loc().Max.Y - tagHeight - if y >= maxy { - return - } - s := co.detach(id) - co.fill() - co.Attach(s, y) -} - -func (co *Col) Handle(e interface{}) { - for i := range co.List { - t := co.List[i] - switch t := t.(type) { - case (*tag.Tag): - t.Handle(t.Body, e) - } - } -} - -// attach inserts w in position id, shifting the original forwards -func (co *Col) attach(w Plane, id int) { - if w == nil || w == co.List[0] || id < 1 { - return - } - co.List = append(co.List[:id], append([]Plane{w}, co.List[id:]...)...) - r := co.List[id-1].Loc() - if len(co.List) > 2 { - w.Move(image.Pt(r.Min.X, r.Max.Y)) - } -} - -func (co *Col) detach(id int) Plane { - if id < 1 || id > len(co.List)-1 { - return nil - } - w := co.List[id] - copy(co.List[id:], co.List[id+1:]) - co.List = co.List[:len(co.List)-1] - return w -} - -func (co *Col) fill() { - if co == nil || co.List[0] == nil { - return - } - var wg sync.WaitGroup - defer wg.Wait() - - ty := co.List[0].Loc().Dy() - - co.List[0].Resize(image.Pt(co.size.X, ty)) - // Tagtext(fmt.Sprintf("id=tagtag r=%s", co.List[0].Loc()), co.List[0]) - - x := co.size.X - y1 := co.Loc().Max.Y - for n := len(co.List) - 1; n > 0; n-- { - n := n - y0 := co.List[n].Loc().Min.Y - wg.Add(1) - pt := image.Pt(x, y1-y0) - go func() { - co.List[n].Resize(pt) - defer wg.Done() - }() - y1 = y0 - // Tagtext(fmt.Sprintf("id=%d r=%s", n, co.List[n].Loc()), co.List[n]) - } + col.Fill(co) } diff --git a/color.go b/color.go new file mode 100644 index 0000000..2e6811c --- /dev/null +++ b/color.go @@ -0,0 +1,61 @@ +package main + +import ( + "github.com/as/edit" + "github.com/as/frame" + "github.com/as/ui/win" +) + +var ( + Tag0 = frame.ATag0 + Tag1 = frame.ATag1 + Tag2 = frame.ATag1 + Body2 = frame.A +) + +func usedarkcolors() { + Body2.Text = frame.MTextW + Body2.Back = frame.MBodyW + + Tag0.Text = frame.MTextW + Tag0.Back = frame.MTagG + + Tag1.Text = frame.MTextW + Tag1.Back = frame.MTagC + + Tag2.Text = frame.MTextW + Tag2.Back = frame.MTagC + + GridConfig.Color[0] = Tag0 + ColConfig.Color[0] = Tag1 + TagConfig.Color[0] = Tag2 + TagConfig.Color[1] = Body2 + + SB := frame.Color{ + Palette: frame.Palette{ + Text: frame.MTagC, + Back: frame.MTagG, + }, + } + GridConfig.Color[2] = SB + ColConfig.Color[2] = SB + TagConfig.Color[2] = SB +} + +func (g *Grid) acolor(e edit.File) { + if t := g.FindName(e.Name); t != nil { + if t.Body == nil { + return + } + win := t.Body.(*win.Win) + if win == nil { + return + } + fr := win.Frame + p0, p1 := e.Q0, e.Q1 + p0 -= clamp(p0-t.Body.Origin(), 0, fr.Len()) + p1 -= clamp(p1-t.Body.Origin(), 0, fr.Len()) + fr.Recolor(fr.PointOf(p0), p0, p1, frame.Mono.Palette) + fr.Mark() + } +} diff --git a/conductor.go b/conductor.go new file mode 100644 index 0000000..0414998 --- /dev/null +++ b/conductor.go @@ -0,0 +1,67 @@ +package main + +/* +import ( + "errors" + "image" + "strings" + + "github.com/as/ui/col" + "github.com/as/ui/tag" +) + +var ( + ErrPlacement = errors.New("placement error") + ErrUnknown = errors.New("unknown placement request") +) + +type evp struct { + dst, src Plane + root Plane +} + +var ( + evpI = make(chan evp) + evpO = make(chan error) +) + +func conduct() { + for { + select { + case evp := <-evpI: + select{ + case evpO <- place(evp): + } + } + } +} + +func place(e evp) error { + switch t := e.src.(type) { + case *tag.Tag: + g, _ := e.root.(*Grid) + c, _ := e.dst.(*col.Col) + if c == nil { + return ErrPlacement + } + if g != nil && strings.HasSuffix(t.FileName(), "+Errors") && len(g.List) > 1 { + c0 := g.List[len(g.List)-1].(*Col) + if c0 != nil { + c = c0 + } + } + + dDY := c.Area().Dy() + if c.Len() > 0 { + last := c.List[len(c.List)-1] + if c.Area().Dy() > last.Loc().Dy()*3 { + logf("should roll up windows--area too small") + } + dDY = last.Loc().Dy() / 2 + } + col.Attach(c, t, image.Pt(dDY, dDY)) + return nil + } + return ErrUnknown +} +*/ diff --git a/config.go b/config.go new file mode 100644 index 0000000..c5b9458 --- /dev/null +++ b/config.go @@ -0,0 +1,46 @@ +package main + +import ( + "image" + + "github.com/as/font" + "github.com/as/frame" + "github.com/as/ui/tag" +) + +var ( + GridConfig = &tag.Config{ + Margin: image.Pt(15, 0), + Filesystem: newfsclient(), + Facer: font.NewFace, + FaceHeight: *ftsize, + Color: [3]frame.Color{ + 0: Tag0, + }, + Ctl: events, + } + + ColConfig = &tag.Config{ + Margin: image.Pt(15, 0), + Filesystem: newfsclient(), + Facer: font.NewFace, + FaceHeight: *ftsize, + Color: [3]frame.Color{ + 0: Tag1, + }, + Ctl: events, + } + + TagConfig = &tag.Config{ + Margin: image.Pt(15, 0), + Filesystem: newfsclient(), + Facer: font.NewFace, + FaceHeight: *ftsize, + Color: [3]frame.Color{ + 0: Tag2, + 1: Body2, + }, + Image: true, + Ctl: events, + } +) diff --git a/cursor.go b/cursor.go index 1c83f15..06ab7d0 100644 --- a/cursor.go +++ b/cursor.go @@ -1,8 +1 @@ package main - -type Cursor struct { - sweep bool - sweepCol bool - srcCol *Col - src Plane -} diff --git a/drag.go b/drag.go new file mode 100644 index 0000000..89f06df --- /dev/null +++ b/drag.go @@ -0,0 +1,72 @@ +package main + +import ( + "image" + "time" + + "github.com/as/ui/col" + "github.com/as/ui/tag" + + "golang.org/x/mobile/event/mouse" +) + +var ( + DragArea = image.Rect(-50, -50, 50, 50) + DragTimeout = time.Second * 1 + down uint +) + +func readmouse(e mouse.Event) mouse.Event { + switch e.Direction { + case 1: + down |= 1 << uint(e.Button) + case 2: + down &^= 1 << uint(e.Button) + } + return e +} + +func dragCol(g *Grid, c *Col, e mouse.Event, mousein <-chan mouse.Event) { + c0 := actCol + for e = range mousein { + e = readmouse(e) + if down == 0 { + break + } + // uncomment for really stupid stuff + //col.Detach(g, g.ID(c0)) + //col.Fill(g) + //col.Attach(g, c0, p(e)) + //g.Upload() + } + col.Detach(g, g.ID(c0)) + col.Fill(g) + col.Attach(g, c0, p(e)) + g.Upload() + moveMouse(c0.Loc().Min) +} + +func dragTag(c *Col, t *tag.Tag, e mouse.Event, mousein <-chan mouse.Event) { + c.Detach(c.ID(t)) + t0 := time.Now() + r0 := DragArea.Add(p(e).Add(t.Bounds().Min)) + for e = range mousein { + e = readmouse(e) + if down == 0 { + break + } + } + pt := p(e) + if time.Since(t0) < DragTimeout && p(e).In(r0) { + pt.Y -= 100 + col.Attach(actCol, t, pt) + } else { + activate(p(e), g) + col.Fill(c) + if t == nil { + return + } + col.Attach(actCol, t, pt) + } + moveMouse(t.Loc().Min) +} diff --git a/edit.go b/edit.go new file mode 100644 index 0000000..85e7c41 --- /dev/null +++ b/edit.go @@ -0,0 +1,28 @@ +package main + +import ( + "github.com/as/edit" + "github.com/as/text" +) + +func (g *Grid) EditRun(prog string, ed text.Editor) (ok bool) { + //TODO(as): danger, edit needs a way to ensure it will only jump to an address + if prog == "" { + return false + } + if ed == nil { + g.aerr("edit: ed == nil") + return false + } + cmd, err := edit.Compile(prog) + if err != nil { + g.aerr("edit: compile: %q: %s", prog, err) + return false + } + err = cmd.Run(ed) + if err != nil { + g.aerr("edit: run: %q: %s", prog, err) + } + + return err == nil +} diff --git a/error.go b/error.go new file mode 100644 index 0000000..fa0dc32 --- /dev/null +++ b/error.go @@ -0,0 +1,29 @@ +package main + +import ( + "log" + "os" +) + +var logFunc = log.Printf + +func init() { + log.SetFlags(log.Llongfile) + log.SetPrefix("a: ") +} + +func logf(fm string, v ...interface{}) { + logFunc(fm, v...) +} + +func ckfault(err error) { + if err == nil { + return + } + logf("fault: %s", err) + os.Exit(1) // TODO(as): if we're in graphical mode, or have files open, we cant do this +} + +func setLogFunc(f func(string, ...interface{})) { + logFunc = f +} diff --git a/frame.go b/frame.go new file mode 100644 index 0000000..e36aaa9 --- /dev/null +++ b/frame.go @@ -0,0 +1,37 @@ +package main + +import ( + "image" + + "github.com/as/font" + "github.com/as/frame" + "github.com/as/shiny/screen" + "github.com/as/ui" +) + +var ( + winSize = image.Pt(1024, 768) +) + +func frameinstall() (ui.Dev, screen.Window, *screen.Device, font.Face) { + if *oled { + usedarkcolors() + } + frame.ForceUTF8 = *utf8 + frame.ForceElastic = *elastic + if *utf8{ + TagConfig.Body.Frame.Flag |= frame.FrUTF8 + } + if *elastic{ + TagConfig.Body.Frame.Flag |= frame.FrElastic + } + dev, err := ui.Init(&screen.NewWindowOptions{ + Width: winSize.X, Height: winSize.Y, + Title: "A", + }) + ckfault(err) + if dev == nil { + panic("no device") + } + return dev, dev.Window(), screen.Dev, font.NewFace(*ftsize) +} diff --git a/fs.go b/fs.go new file mode 100644 index 0000000..44a6246 --- /dev/null +++ b/fs.go @@ -0,0 +1,86 @@ +package main + +import ( + "fmt" + "os" + "path" + "path/filepath" + "strings" + + "github.com/as/srv/fs" +) + +type fileresolver struct { + fs.Fs + err error +} + +type pathinfo struct { + root string + tag string + pred string +} + +type fileinfo struct { + os.FileInfo + path string + dir string +} + +func (r *fileresolver) set(f *fileinfo, name string) (ok bool) { + f.path = name + f.dir = name + if f.FileInfo != nil && !f.FileInfo.IsDir() { + f.dir = filepath.Dir(f.path) + } + return r.err == nil +} + +func (f *fileinfo) String() string { + if f == nil { + return "" + } + return fmt.Sprint(f.FileInfo) +} + +func (r *fileresolver) isAbs(name string) bool { + const Letters = "cCABDEFGHIJKLMNOPQRSTUVWXYZabdefghijklmnopqrstuvwxyz" + if len(name) == 0 { + return false + } + if path.IsAbs(name) || filepath.IsAbs(name) || name[0] == '\\' || name[0] == '/' { + return true + } + return len(name) > 1 && name[1] == ':' && strings.ContainsAny(name[:1], Letters) +} + +// joindir resolves a to a file or directory and runs filepath.Join on +// the directory of a. If a is already a directory the operation is +// join(a,b), if it's a file the operation is join(a/.., b). +func (r *fileresolver) joindir(a, b string) string { + info, err := r.Stat(a) + if err != nil || !info.IsDir() { + a = filepath.Dir(a) + } + return filepath.Join(a, b) +} + +func (r *fileresolver) look(pi pathinfo) (f fileinfo, ok bool) { + if r.isAbs(pi.pred) { + // last element is absolute; + f.FileInfo, r.err = r.Stat(pi.pred) + return f, r.set(&f, filepath.Clean(pi.pred)) + } + + if r.isAbs(pi.tag) { + file := r.joindir(pi.tag, pi.pred) + f.FileInfo, r.err = r.Stat(file) + return f, r.set(&f, filepath.Clean(file)) + } + + // need to know if + pi.tag = filepath.Join(pi.root, pi.tag) + file := r.joindir(pi.tag, pi.pred) + f.FileInfo, r.err = r.Stat(file) + return f, r.set(&f, filepath.Clean(file)) +} diff --git a/go.mod b/go.mod index e66ae91..b76980e 100644 --- a/go.mod +++ b/go.mod @@ -11,10 +11,10 @@ require ( github.com/as/io v0.0.0-20170912233418-6e653d50e75b github.com/as/ms v0.0.0-20180411144820-24d8984d31f7 github.com/as/path v0.6.7 - github.com/as/shiny v0.5.5 + github.com/as/shiny v0.6.7 github.com/as/srv v0.6.7 - github.com/as/text v0.5.5 - github.com/as/ui v0.5.5 + github.com/as/text v0.6.7 + github.com/as/ui v0.6.8 github.com/as/worm v0.6.7 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 golang.org/x/image v0.0.0-20180403161127-f315e4403028 diff --git a/grid.go b/grid.go index 9a3bbfe..1280a39 100644 --- a/grid.go +++ b/grid.go @@ -1,61 +1,39 @@ package main import ( + "image" + "github.com/as/edit" - "github.com/as/font" "github.com/as/frame" "github.com/as/text" "github.com/as/ui" + "github.com/as/ui/col" "github.com/as/ui/tag" "github.com/as/ui/win" - "image" ) type Grid struct { - *Col + col.Table2 } -func NewGrid(dev *ui.Dev, sp, size image.Point, ft font.Face, files ...string) *Grid { - N := len(files) - tdy := tag.TagSize(ft) - tagpad := tag.TagPad(pad) - conf := &tag.Config{ - Margin: tagpad, - Facer: font.NewFace, - FaceHeight: ft.Height(), - Color: [3]frame.Color{ - 0: frame.ATag0, - }, - } - T := tag.New(dev, sp, image.Pt(size.X, tdy), conf) - T.Win.InsertString("Newcol Killall Exit", 0) - g := &Grid{&Col{dev: dev, sp: sp, size: size, ft: ft, Tag: T, tdy: tdy, List: make([]Plane, len(files))}} - size.Y -= tdy - sp.Y += tdy - d := image.Pt(size.X/N, size.Y) - for i, v := range files { - g.List[i] = NewCol(dev, ft, sp, size, v) - sp.X += d.X - } - g.List = append([]Plane{T}, g.List...) - g.Refresh() +var ( + GridLabel = "Newcol Killall Exit guru^(callees callers callstack definition describe freevars implements peers pointsto referrers what whicherrs)" +) + +func NewGrid(dev ui.Dev, conf *tag.Config, files ...string) *Grid { + g := &Grid{col.NewTable2(dev, conf)} + g.Tag.Win.InsertString(GridLabel, 0) return g } -func (g *Grid) Attach(src Plane, x int) { - did := g.IDPoint(image.Pt(x, g.sp.Y+g.tdy)) - if did != 0 && did < len(g.List) { - d := g.List[did] - x := x - d.Loc().Min.X - y := sizeof(d.Loc()).Y - d.Resize(image.Pt(x, y)) - } - g.attach(src, did+1) - g.fill() +func (g *Grid) Move(sp image.Point) { + g.Table2.Move(sp) } -func (g *Grid) Move(sp image.Point) { - panic("never call this") +func (g *Grid) Resize(size image.Point) { + g.ForceSize(size) + g.Tag.Resize(image.Pt(size.X, g.Config.TagHeight())) + col.Fill(g) } // Install places the given edit script in between @@ -72,7 +50,10 @@ func (g *Grid) Move(sp image.Point) { // ,x,string,h // Any other use is undefined and untested for now func (g *Grid) Install(t *tag.Tag, srcprog string) { - + w, _ := t.Body.(*win.Win) + if w == nil { + return + } var green = frame.Palette{ Back: frame.Green, Text: frame.A.Text, @@ -84,53 +65,14 @@ func (g *Grid) Install(t *tag.Tag, srcprog string) { return } - if t.Body != nil { - t.Body.FuncInstall(func(w *win.Win) { - fr := w.Frame - buf := text.BufferFrom(w.Bytes()[w.Origin() : w.Origin()+fr.Len()]) - ed, _ := text.Open(buf) - prog.Run(ed) - for _, dot := range prog.Emit.Dot { - w.Frame.Recolor(fr.PointOf(dot.Q0), dot.Q0, dot.Q1, green) - } - //prog.Emit = &edit.Emitted{} - }) - } -} - -func (g *Grid) Resize(size image.Point) { - g.size = size - g.fill() -} - -// attach inserts w in position id, shifting the original right -func (g *Grid) attach(w Plane, id int) { - if id < 1 { - return - } - g.List = append(g.List[:id], append([]Plane{w}, g.List[min(id, len(g.List)):]...)...) - r := g.List[id-1].Loc() - if id-1 == 0 { - r = image.Rect(g.sp.X, g.sp.Y+g.tdy, g.sp.X, g.sp.Y+g.size.Y) - } - w.Move(image.Pt(r.Max.X, g.sp.Y+g.tdy)) -} - -func (g *Grid) fill() { - tdy := g.tdy - g.List[0].Resize(image.Pt(g.size.X, tdy)) - y := g.size.Y - tdy - x1 := g.Loc().Max.X - for n := len(g.List) - 1; n > 0; n-- { - x0 := g.List[n].Loc().Min.X - g.List[n].Resize(image.Pt(x1-x0, y)) - x1 = x0 - } -} - -func min(a, b int) int { - if a < b { - return a - } - return b + w.FuncInstall(func(w *win.Win) { + fr := w.Frame + buf := text.BufferFrom(w.Bytes()[w.Origin() : w.Origin()+fr.Len()]) + ed, _ := text.Open(buf) + prog.Run(ed) + for _, dot := range prog.Emit.Dot { + w.Frame.Recolor(fr.PointOf(dot.Q0), dot.Q0, dot.Q1, green) + } + //prog.Emit = &edit.Emitted{} + }) } diff --git a/guru.go b/guru.go new file mode 100644 index 0000000..4f08751 --- /dev/null +++ b/guru.go @@ -0,0 +1,81 @@ +package main + +import ( + "fmt" + "os/exec" + "strings" + "time" + + "github.com/as/ui/tag" +) + +var guruModes = "callees callers callstack definition describe freevars implements peers pointsto referrers what whicherrs" + +func addrfmt(label string, q0, q1 int64) string { + if q0 == q1 { + return fmt.Sprintf("%s:#%d", label, q1) + } + return fmt.Sprintf("%s:#%d,#%d", label, q0, q1) +} + +func (g *Grid) afindguru(wd string, name string) *tag.Tag { + if !strings.HasSuffix(name, "+Guru") { + name += "+Guru" + } + t := g.FindName(name) + if t == nil { + c := g.List[len(g.List)-1].(*Col) + t = New(c, "", name).(*tag.Tag) + if t == nil { + panic("cant create tag") + } + } + return t +} + +func (g *Grid) aguru(fm string, i ...interface{}) { + t := g.afindguru(".", "") + q1 := t.Body.Len() + t.Body.Select(q1, q1) + n := int64(t.Body.Insert([]byte(time.Now().Format(timefmt)+": "+fmt.Sprintf(fm, i...)), q1)) + t.Body.Select(q1+n, q1+n) + ajump(t.Body, cursorNop) +} + +func (g *Grid) Selection() string { + return string(g.Tag.Rdsel()) +} + +func (g *Grid) guru(label string, q0, q1 int64) (advance bool, err error) { + if strings.HasSuffix(label, ".go") { + return true, nil + } + asel := g.Selection() + mode := "" + // scope := "." + for _, v := range strings.Fields(guruModes) { + if asel == v { + mode = v + break + } + } + if mode == "" { + return true, nil + } + + data, err := exec.Command( + "guru", + // "-scope", + // scope, + mode, + addrfmt(label, q0, q1), + ).CombinedOutput() + + if err != nil { + g.aerr("guru: %s", err) + } + if len(data) != 0 { + g.aguru("%s", data) + } + return false, err +} diff --git a/imports.go b/imports.go new file mode 100644 index 0000000..deae4f2 --- /dev/null +++ b/imports.go @@ -0,0 +1,65 @@ +package main + +import ( + "bytes" + "fmt" + "os/exec" + "strings" + + "github.com/as/ui/tag" + "golang.org/x/mobile/event/key" +) + +type ErrGoImports struct { + Path string + GoExt bool + Err error +} + +func (e ErrGoImports) Error() string { + m := e.Err.Error() + if m == "" && !e.GoExt { + m = "not a go file" + } + n := strings.Index(m, "") + if n != -1 { + m = strings.Replace(m, "", e.Path, -1) + } else { + m = e.Path + ":" + m + } + return fmt.Sprintf("goimports: %s", m) +} + +func runGoImports(t *tag.Tag, e key.Event) { + ee := &ErrGoImports{ + Path: t.FileName(), + GoExt: !strings.HasSuffix(t.FileName(), ".go"), + } + cmd := exec.Command("goimports") + cmd.Stdin = bytes.NewReader(t.Body.Bytes()) + b := new(bytes.Buffer) + berr := new(bytes.Buffer) + cmd.Stdout = b + cmd.Stderr = berr + err := cmd.Run() + if err != nil || b.Len() < len("package") { + if err == nil { + ee.Err = fmt.Errorf("file too short") + } else { + if berr.Len() != 0 { + ee.Err = fmt.Errorf(berr.String()) + } else { + ee.Err = err + } + } + if ee != nil { + logf("imports: %s", ee) + } + return + } + q0, q1 := t.Body.Dot() + t.Body.Delete(0, t.Body.Len()) + t.Body.Insert(b.Bytes(), 0) + t.Mark() + t.Select(q0, q1) +} diff --git a/kbd.go b/kbd.go new file mode 100644 index 0000000..dc425e0 --- /dev/null +++ b/kbd.go @@ -0,0 +1,86 @@ +package main + +import ( + "image" + + "github.com/as/font" + "github.com/as/shiny/screen" + "github.com/as/text/find" + "github.com/as/text/kbd" + "github.com/as/ui/tag" + "github.com/as/ui/win" + "golang.org/x/mobile/event/key" +) + +type Window interface { + Bytes() []byte + + Select(q0, q1 int64) + SetOrigin(org int64, exact bool) + Dot() (int64, int64) + Len() int64 + Origin() int64 + + Scroll(dl int) + Insert(p []byte, q0 int64) (n int) + // WriteAt(p []byte, at int64) (n int, err error) + Delete(q0, q1 int64) (n int) + + Fill() + Blank() + Refresh() + Dirty() bool + Loc() image.Rectangle + Move(image.Point) + Resize(image.Point) + Close() error + Upload() + Window() screen.Window +} + +func kbdin(e key.Event, t *tag.Tag, act Window) { + if e.Direction == 2 { + return + } + if e.Code == key.CodeI && e.Modifiers == key.ModControl { + runGoImports(t, e) + return + } + switch e.Code { + case key.CodeEqualSign, key.CodeHyphenMinus: + if e.Modifiers == key.ModControl { + win, _ := t.Body.(*win.Win) + if win == nil { + return + } + size := win.Frame.Face.Height() + if key.CodeHyphenMinus == e.Code { + size -= 1 + } else { + size += 1 + } + if size < 3 { + size = 6 + } + t.SetFont(font.NewFace(size)) + return + } + } + ntab := int64(-1) + if (e.Rune == '\n' || e.Rune == '\r') && act == t.Body { + q0, q1 := act.Dot() + if q0 == q1 { + p := act.Bytes() + l0, _ := find.Findlinerev(p, q0, 0) + ntab = find.Accept(p, l0, []byte{'\t'}) + ntab -= l0 + 1 + } + } + kbd.SendClient(act, e) + for ntab >= 0 { + e.Rune = '\t' + kbd.SendClient(act, e) + ntab-- + } + t.Mark() +} diff --git a/limit.go b/limit.go new file mode 100644 index 0000000..157c26e --- /dev/null +++ b/limit.go @@ -0,0 +1,22 @@ +package main + +import ( + "time" + + "golang.org/x/time/rate" +) + +var ( + Limit = rate.Limit(time.Second / 120) + Request = 2 +) + +var lim = rate.NewLimiter(Limit, Request) + +// There was once a time where the repainting had to be rate +// limited. This currently happens when processing a request +// to repaint a section of the grid + +func throttled() bool { + return !lim.Allow() +} diff --git a/look.go b/look.go index e881c19..32677da 100644 --- a/look.go +++ b/look.go @@ -1,6 +1,7 @@ package main import ( + "errors" "fmt" "image" "os" @@ -8,9 +9,7 @@ import ( "strings" "time" - "github.com/as/edit" "github.com/as/event" - "github.com/as/path" "github.com/as/text" "github.com/as/text/action" "github.com/as/text/find" @@ -18,6 +17,19 @@ import ( "github.com/as/ui/win" ) +type Named interface { + Plane + FileName() string +} +type Indexer interface { + Lookup(interface{}) Plane +} + +var ( + ErrNoWin = errors.New("no window") + ErrNoTag = errors.New("no tag") +) + func AbsOf(basedir, path string) string { if filepath.IsAbs(path) { return path @@ -25,119 +37,153 @@ func AbsOf(basedir, path string) string { return filepath.Join(basedir, path) } -// Looks are done in the following order -// 1). Tag name - if it exists already jump to the tag, if there's an address jump -// to that address in the tag -// -// 2). Readable absolute file - if the name matches a readable file in the namespaces -// file system -// -// 3). Readable relative file - if the name matches the name of a readable file in -// the tags base directory -// -// Address lookups follow for each of the three above -// -// 4). An address in the destination +var resolver = &fileresolver{ // from fs.go:/resolver/ + Fs: newfsclient(), // called in :/Grid..Look/ +} -func (g *Grid) Look(e event.Look) { - name, addr := action.SplitPath(string(e.P)) - if g.meta(e.To[0]) { - return +func lookTarget(current tag.Window, t *tag.Tag) tag.Window { + if t == nil || current == nil { + return nil + } + if current == t.Win { + return t.Body + } + return current +} + +type Looker struct { + *tag.Tag // owning tag + *win.Win // source of the address below + Q0, Q1 int64 + P []byte + err error +} + +func (e *Looker) Err() error { + if e.err == nil { + if e.Win == nil { + e.err = ErrNoWin + } + if e.Tag == nil { + e.err = ErrNoTag + } + } + return e.err +} + +func (l *Looker) FromTag() bool { + return l.Tag.Win == l.Win +} + +func (e *Looker) SplitAddr() (name, addr string) { + e.Q0, e.Q1 = expand3(e.Win, e.Q0, e.Q1) + return action.SplitPath(string(e.Win.Bytes()[e.Q0:e.Q1])) +} + +/* +func (e *Looker) LookGrid(g *Grid) (error) { + panic("unfinished") + if e.Err() != nil { + return e.Err() } + + name, addr := e.SplitAddr() if name == "" && addr == "" { - return + return nil } + if name == "" { - prog, err := edit.Compile(addr) - if err != nil { - g.aerr(err.Error()) - return + if g.EditRun(addr, e.Tag.Body) { + ajump(e.Tag.Body, cursorNop) } - t := e.To[0] - prog.Run(t) - ajump(t, cursorNop) - return + return nil } // Existing window label? - if label := g.Lookup(name); label != nil { - fn := moveMouse - if name == "" { - fn = cursorNop - } - //TODO(as): danger, edit needs a way to ensure it will only jump to an address - // we can expose an address parsing function from edit - prog, err := edit.Compile(addr) - if err != nil { - g.aerr(err.Error()) + if label := g.Lookup(name); label != nil { + t, _ := label.(*tag.Tag) + if t == nil{ + logf("look d: tag is nil") return } - if t := label.(*tag.Tag); t.Body != nil { - prog.Run(t.Body) - ajump(t.Body, fn) + if g.EditRun(addr, t.Body) { + ajump(t.Body, moveMouse) } return } - abspath := "" - visible := "" - exists := false - switch { - case filepath.IsAbs(name): - if !path.Exists(name) { - break - } - abspath = filepath.Clean(name) - visible = abspath - case filepath.IsAbs(e.Name): - if !path.Exists(e.Name) { - // The tag might point to a non-existent file but its parent directory - // might be valid. In this case we should look inside the directory - // even though the file doesn't exist. This makes +Error windows work - // as intended - e.Name = filepath.Dir(e.Name) + // A file on the filesystem + info, exists := resolver.look(pathinfo{tag: e.Name, name: name}) + t, exists = g.Lookup(info.abspath).(*tag.Tag) + + + panic("unfinished") +} +*/ + +func (g *Grid) cwd() string { + s, _ := os.Getwd() + return s +} +func (g *Grid) Look(e event.Look) { + if g.meta(g.Tag) { + return + } + + ed := e.To[0] + t, _ := ed.(*tag.Tag) + + e.Q0, e.Q1 = expand3(ed, e.Q0, e.Q1) + name, addr := action.SplitPath(string(e.P)) + // e.P = ed.Bytes()[e.Q0:e.Q1] + if name == "" && addr == "" { + return + } + + if matches(httpLink.Plumb(&Plumbmsg{Data: e.P})) { + return + } + if name == "" { + if t == nil { + return } - if !path.Exists(e.Name) { - break + if t.Body == nil { + return } - abspath = filepath.Join(path.DirOf(e.Name), name) - visible = abspath - case filepath.IsAbs(e.Basedir): - if !path.Exists(e.Basedir) { - break + if g.EditRun(addr, t.Body) { + ajump(ed, cursorNop) } - abspath = path.DirOf(e.Basedir) - visible = filepath.Join(path.DirOf(e.Name), name) - default: + return } - stat := func(name string) os.FileInfo { - fi, _ := os.Stat(name) - return fi - } - VisitAll(g, func(p Named) { - if abspath == p.FileName() { - exists = true - } else if os.SameFile(stat(abspath), stat(p.FileName())) { - exists = true + if label := g.Lookup(name); label != nil { + // TODO(as): This fails to find labels like +Error + // and is overall useless + t, _ := label.(*tag.Tag) + if t == nil { + logf("nil tag") + return } - }) - - var t *tag.Tag - if exists { - q := g.Lookup(abspath) - if q, ok := q.(*tag.Tag); ok { - t = q + if t.Body == nil || !g.EditRun(addr, t.Body) { + ajump(t, cursorNop) + return + } else if t.Body != nil { + ajump(t.Body, moveMouse) + return } - } else if abspath == visible && path.Exists(visible) { - t = New(actCol, path.DirOf(abspath), visible).(*tag.Tag) - } else if realpath := filepath.Join(abspath, visible); path.Exists(realpath) { - t = New(actCol, path.DirOf(abspath), visible).(*tag.Tag) + ajump(t, moveMouse) + return + } + + exists := false + info, existsRemote := resolver.look(pathinfo{root: g.cwd(), tag: e.Name, pred: name}) + + t, exists = g.Lookup(info.path).(*tag.Tag) + if !exists && existsRemote { + t, _ = New(actCol, info.dir, info.path).(*tag.Tag) } if t != nil { - if addr != "" { - //TODO(as): danger, edit needs a way to ensure it will only jump to an address - edit.MustCompile(addr).Run(t.Body) + if g.EditRun(addr, t.Body) { ajump(t.Body, moveMouse) } else { ajump(t, moveMouse) @@ -145,41 +191,56 @@ func (g *Grid) Look(e event.Look) { return } + if !exists && !existsRemote { + advance, _ := g.guru(e.Name, e.Q0, e.Q1) + if !advance { + return + } + } + //TODO(as): fix this so it doesn't compare hard coded coordinates if e.To[0].(*win.Win) == nil || e.To[0].(Plane).Loc().Max.Y < 48 { VisitAll(g, func(p Named) { if p == nil { return } - lookliteral(p.(*tag.Tag).Body, e.P, cursorNop) + lookliteral(p.(*tag.Tag).Body, e, cursorNop) }) } else { - lookliteral(e.To[0], e.P, moveMouse) + what := e.P + if e.To[0] != e.From { + lookliteraltag(e.To[0], e.Q0, e.Q1, what) + } else { + lookliteral(e.To[0], e, moveMouse) + } } - } func (g *Grid) afinderr(wd string, name string) *tag.Tag { + name = strings.TrimSpace(name) if !strings.HasSuffix(name, "+Errors") { name += "+Errors" } t := g.FindName(name) if t == nil { c := g.List[len(g.List)-1].(*Col) - t = New(c, "", name, SizeThirdOf).(*tag.Tag) + t = New(c, "", name).(*tag.Tag) if t == nil { panic("cant create tag") } - moveMouse(t.Loc().Min) + //moveMouse(t.Loc().Min) } return t } func (g *Grid) aerr(fm string, i ...interface{}) { t := g.afinderr(".", "") + if t == nil || t.Body == nil { + return + } q1 := t.Body.Len() t.Body.Select(q1, q1) n := int64(t.Body.Insert([]byte(time.Now().Format(timefmt)+": "+fmt.Sprintf(fm, i...)+"\n"), q1)) - t.Body.Select(q1, q1+n) - t.Body.Jump(cursorNop) + t.Body.Select(q1+n, q1+n) + ajump(t.Body, cursorNop) } func (g *Grid) aout(fm string, i ...interface{}) { t := g.afinderr(".", "") @@ -187,30 +248,61 @@ func (g *Grid) aout(fm string, i ...interface{}) { t.Body.Select(q1, q1) n := int64(t.Body.Insert([]byte(fmt.Sprintf(fm, i...)+"\n"), q1)) t.Body.Select(q1, q1+n) - t.Body.Jump(cursorNop) + ajump(t.Body, cursorNop) +} + +// expand3 return (r0:r1) if and only if that range is wide and +// not inside ed's dot, otherwise it returns dot +func expand3(ed text.Editor, r0, r1 int64) (int64, int64) { + q0, q1 := ed.Dot() + if r0 == r1 && text.Region3(r0, q0, q1) == 0 { + return q0, q1 + } + return r0, r1 } -func lookliteral(ed text.Editor, p []byte, mouseFunc func(image.Point)) { - // String literal - q0, q1 := find.FindNext(ed, p) + +func lookliteraltag(ed text.Editor, q0, q1 int64, what []byte) { + q0, q1 = ed.Dot() + s0, s1 := find.FindNext(ed, q0, q1, what) + ed.Select(s0, s1) + ajump(ed, nil) +} + +func lookliteral(ed text.Editor, e event.Look, mouseFunc func(image.Point)) { + // The behavior of look: + // + // Independent of the dot range, mark the given range as the starting point. + // Advance to the end of the starting point + // Search for the value repsesenting the range under the original starting point. + // If the found range is identical to the starting point, no result has been found + + t0, t1 := ed.Dot() + // g.aerr("lookliteral: dot(%d:%d)", t0, t1) + // g.aerr("lookliteral: find(%d:%d) [%q]", e.Q0, e.Q1, e.P) + q0, q1 := find.FindNext(ed, e.Q0, e.Q1, e.P) + // g.aerr("lookliteral: next(%d:%d)", q0, q1) + if q0 == e.Q0 && q1 == e.Q1 { + // g.aerr("lookliteral: not found, same output(%d:%d)", q0, q1) + ed.Select(t0, t1) + return + } + // g.aerr("lookliteral: found, diff output(%d:%d) != input(%d:%d)", q0, q1, e.Q0, e.Q1) ed.Select(q0, q1) ajump(ed, mouseFunc) } func (g *Grid) meta(p interface{}) bool { - if w, ok := p.(*win.Win); ok { - return w == g.List[0].(*tag.Tag).Win - } - return false + return p == g.Tag.Win } func VisitAll(root Plane, fn func(p Named)) { + type List interface { + Kids() []Plane + } + switch root := root.(type) { - case *Grid: - for _, k := range root.List[1:] { - VisitAll(k, fn) - } - case *Col: - for _, k := range root.List[1:] { + case List: + for _, k := range root.Kids() { VisitAll(k, fn) } case Named: @@ -221,16 +313,22 @@ func VisitAll(root Plane, fn func(p Named)) { } } -func (col *Col) Kids() []Plane { - return col.List -} - -func (col *Col) Dirty() bool { - return true +func (g *Grid) FindName(name string) *tag.Tag { + for _, p := range g.List { + c, _ := p.(*Col) + if c == nil { + continue + } + t := c.FindName(name) + if t != nil { + return t + } + } + return nil } -func (grid *Grid) Lookup(pid interface{}) Plane { - for _, k := range grid.Kids() { +func (g *Grid) Lookup(pid interface{}) Plane { + for _, k := range g.Kids() { if k, ok := k.(Indexer); ok { tag := k.Lookup(pid) if tag != nil { @@ -240,80 +338,3 @@ func (grid *Grid) Lookup(pid interface{}) Plane { } return nil } - -func (col *Col) Lookup(pid interface{}) Plane { - kids := col.Kids() - if len(kids) == 0 { - return nil - } - switch pid := pid.(type) { - case int: - if pid >= len(kids) { - pid = len(kids) - 1 - } - return col.Kids()[pid] - case string: - for i, v := range col.Kids() { - if v, ok := v.(Named); ok { - if v.FileName() == pid { - return col.Kids()[i] - } - } - } - case image.Point: - return ptInAny(pid, col.Kids()...) - case interface{}: - panic("") - } - return nil -} - -type Named interface { - Plane - FileName() string -} -type Indexer interface { - Lookup(interface{}) Plane -} - -func ptInPlane(pt image.Point, p Plane) bool { - if p == nil { - return false - } - return pt.In(p.Loc()) -} - -func ptInAny(pt image.Point, list ...Plane) (x Plane) { - for i, w := range list { - if ptInPlane(pt, w) { - return list[i] - } - } - return nil -} - -func ajump(p interface{}, cursor func(image.Point)) { - switch p := p.(type) { - case *tag.Tag: - if p != nil { - cursor(p.Loc().Min) - } - case text.Jumper: - p.Jump(cursor) - case Plane: - if cursor == nil { - cursor = shouldCursor(p) - } - cursor(p.Loc().Min) - } -} - -func cursorNop(p image.Point) {} -func shouldCursor(p Plane) (fn func(image.Point)) { - switch p.(type) { - case Named: - return cursorNop - default: - return moveMouse - } -} diff --git a/main.go b/main.go index b0704d1..c996c31 100644 --- a/main.go +++ b/main.go @@ -1,535 +1,215 @@ package main // import "github.com/as/a" import ( - "bytes" "flag" "fmt" "image" - "io" "log" "os" - "os/exec" - "path/filepath" - "strings" - "sync" - "time" + "runtime" - "github.com/as/event" "github.com/as/shiny/screen" mus "github.com/as/text/mouse" - "golang.org/x/mobile/event/key" "golang.org/x/mobile/event/lifecycle" - "golang.org/x/mobile/event/mouse" - "golang.org/x/mobile/event/paint" - "golang.org/x/mobile/event/size" - "golang.org/x/time/rate" "github.com/as/edit" - "github.com/as/font" ///"git - "github.com/as/frame" - "github.com/as/path" - "github.com/as/text" - "github.com/as/ui" + "github.com/as/ui/col" "github.com/as/ui/tag" - "github.com/as/ui/win" - // "github.com/as/font/vga" - // "golang.org/x/image/font/plan9font" ) var ( - Version = "0.5.5" - xx Cursor - eprint = fmt.Println - timefmt = "2006.01.02 15.04.05" - winSize = image.Pt(1024, 768) - pad = image.Pt(15, 15) - tagHeight = *ftsize*2 + *ftsize/2 - 2 - scrollX = 10 + Version = "0.6.7" + eprint = fmt.Println + timefmt = "15.04.05" ) -func p(e mouse.Event) image.Point { - return image.Pt(int(e.X), int(e.Y)) -} - -var focused = false +var ( + g *Grid + D *screen.Device + events = make(chan interface{}, 301) + done = make(chan bool) + moribound = make(chan bool, 1) + sigterm = make(chan bool) + focused = false +) -func argparse() (list []string) { - if len(flag.Args()) > 0 { - list = append(list, flag.Args()...) - } else { - list = append(list, "guide") - list = append(list, ".") +func defaultFaceSize() int { + switch runtime.GOOS { + case "darwin": + return 13 + default: + return 11 } - return } var ( - utf8 = flag.Bool("u", false, "enable utf8 experiment") - elastic = flag.Bool("elastic", false, "enable elastic tabstops") - oled = flag.Bool("b", false, "OLED display mode (black)") - ftsize = flag.Int("ftsize", 11, "font size") + utf8 = flag.Bool("u", false, "enable utf8 experiment") + elastic = flag.Bool("elastic", false, "enable elastic tabstops") + oled = flag.Bool("b", false, "OLED display mode (black)") + ftsize = flag.Int("ftsize", defaultFaceSize(), "font size") + srvaddr = flag.String("l", "", "(dangerous) announce and serve file system clients on given endpoint") + clientaddr = flag.String("d", "", "dial to a remote file system on endpoint") + quiet = flag.Bool("q", false, "dont interact with the graphical subsystem (use with -l)") ) -/* -func vgaface() font.Face { - data, err := ioutil.ReadFile("u_vga16.font") - if err != nil { - panic(err) - } - face, err := plan9font.ParseFont(data, ioutil.ReadFile) - if err != nil { - panic(err) - } - return face -} -*/ -// TODO(as): refactor frame so this stuff doesn't have to exist here -func black() { - frame.A.Text = frame.MTextW - frame.A.Back = frame.MBodyW - - frame.ATag0.Text = frame.MTextW - frame.ATag0.Back = frame.MTagG +func init() { + // this grants the capability to shut down the program + // it happens exactly once + moribound <- true - frame.ATag1.Text = frame.MTextW - frame.ATag1.Back = frame.MTagC + // error.go:/logFunc/ + log.SetFlags(log.Llongfile) + log.SetPrefix("a: ") - // tag.Gray = image.NewUniform(color.RGBA{192, 192, 232, 255}) - // tag.LtGray = image.NewUniform(color.RGBA{192, 192, 232, 255}) - // tag.X = image.NewUniform(color.RGBA{192, 192, 232, 255}) + flag.Parse() } -var dirty bool - -func ck() { - if dirty || (act != nil && act.Dirty()) { - act.Window().Send(paint.Event{}) - } +func banner() { + logf("ver=%s", Version) + logf("pid=%d", os.Getpid()) + logf("args=%q", os.Args) + repaint() } -var g *Grid - -// Put func main() { - flag.Parse() defer trypprof()() - frame.ForceUTF8 = *utf8 - frame.ForceElastic = *elastic - - lim := rate.NewLimiter(rate.Every(time.Second/120), 2) - - if *oled { - black() - } - list := argparse() - dev, err := ui.Init(&screen.NewWindowOptions{Width: winSize.X, Height: winSize.Y, Title: "A"}) - if err != nil { - log.Fatalln(err) + if *quiet { + banner() + createnetworks() + <-done + os.Exit(0) } - wind := dev.Window() - - // Linux will segfault here if X is not present - wind.Send(paint.Event{}) - ft := font.NewFace(*ftsize) - g = NewGrid(dev, image.ZP, winSize, ft, list...) - // This in particular needs to go - actCol = g.List[1].(*Col) - actTag = actCol.List[1].(*tag.Tag) - act = actTag.Body + dev, wind, d, ft := frameinstall() + D = d - var pt image.Point - r := act.Bounds() - mousein := mus.NewMouse(time.Second/3, wind) - mousein.Machine.SetRect(image.Rect(r.Min.X, r.Min.Y+pad.Y, r.Max.X, r.Max.Y-pad.Y)) + g = NewGrid(dev, GridConfig) + sp, size := image.Pt(0, 0), image.Pt(900, 900) + g.Move(sp) + g.Resize(size) - // - // Temporary just until col and tag can be seperated into their own packages. The - // majority of these closures will disappear as the program becomes more stable - sweepend := func() { - if xx.sweepCol { - xx.sweepCol = false - g.fill() - g.Attach(xx.srcCol, pt.X) - moveMouse(xx.srcCol.Loc().Min) - } else { - xx.sweep = false - xx.srcCol.fill() - if xx.src == nil { - return - } - actCol.Attach(xx.src, pt.Y) - moveMouse(xx.src.Loc().Min) - } + for _, v := range list { + col.Attach(g, NewCol(dev, ft, image.ZP, image.ZP, v), sp) + sp.X += size.X / len(list) } - // Returns the bounding box of the invisible sizer you can use - // to draw windows and columns around. - sizerOf := func(p Plane) image.Rectangle { - r := p.Loc() - r.Max = r.Min.Add(image.Pt(scrollX, tagHeight)) - return r - } - sizerHit := func(p Plane, pt image.Point) bool { - in := pt.In(sizerOf(p)) - return in - } - markwin := func() { - xx.srcCol = actCol - xx.src = actTag - } - detachcol := func() { - xx.srcCol = actCol - xx.sweepCol = true - xx.src = nil - g.detach(g.ID(xx.srcCol)) - g.fill() - } - detachwin := func() { - markwin() - xx.srcCol.detach(xx.srcCol.ID(xx.src)) - } - growshrink := func(e mouse.Event) { - dy := r.Min.Y - id := actCol.ID(actTag) - switch e.Button { - case 3: - actCol.RollUp(id, dy) - //actCol.MoveWin(id, dy) - case 2: - dy -= *ftsize * 2 - actCol.MoveWin(id, dy) - case 1: - actCol.Grow(id, actCol.bestGrowth(id, tagHeight)) - } - moveMouse(actTag.Loc().Min) - } - tophit := func() bool { - return pt.Y > g.sp.Y+g.tdy && pt.Y < g.sp.Y+g.tdy*2 - } - - ajump := func(ed text.Editor, cursor bool) { - fn := moveMouse - if !cursor { - fn = nil - } - if ed, ok := ed.(text.Jumper); ok { - ed.Jump(fn) - } - } - alook := func(e event.Look) { - g.Look(e) - } - aerr := g.aerr - var ( - scrollbar = 1 - sizer = 2 - window = 4 - cont = 0 - ) + col.Fill(g) + g.Refresh() - aerr("ver=%s", Version) - aerr("pid=%d", os.Getpid()) - aerr("args=%q", os.Args) - - for { - e := wind.NextEvent() - switch e := e.(type) { - case mus.Drain: - DrainLoop: - for { - switch wind.NextEvent().(type) { - case mus.DrainStop: - break DrainLoop - } - } - case tag.GetEvent: - t := New(actCol, e.Basedir, e.Name) - if e.Addr != "" { - actTag = t.(*tag.Tag) - act = actTag.Body - actTag.Handle(actTag.Body, edit.MustCompile(e.Addr)) - p0, _ := act.Frame.Dot() - moveMouse(act.Loc().Min.Add(act.PointOf(p0))) - } else { - moveMouse(t.Loc().Min) - } - case mouse.Event: - pt = p(e).Add(act.Loc().Min) - if cont == 0 { + setLogFunc(g.aerr) + banner() + createnetworks() + actinit(g) + assert("actinit", g) + go func() { + for { + select { + case e := <-D.Scroll: activate(p(e), g) - } - e.X -= float32(act.Sp.X) - e.Y -= float32(act.Sp.Y) - mousein.Sink <- e - case mus.MarkEvent: - - cont = 0 - pt = p(e.Event).Add(act.Loc().Min) - if sizerHit(actTag, pt) { - if e.Button == 1 { - if tophit() { - detachcol() - } else { - detachwin() - } - cont = sizer - } else { - growshrink(e.Event) - } - - } else if x := int(e.X); x >= 0 && x < 10 { - cont = scrollbar - act.Clicksb(p(e.Event), int(e.Button)) - } else { - actTag.Handle(act, e) - cont = window - } - ck() - case mus.ScrollEvent: - doScrollEvent(act, e) - case mus.SweepEvent: - switch cont { - case scrollbar: - act.Clicksb(p(e.Event), 0) - case sizer: - case window: - if !e.Motion() && act != nil { - r := act.Frame.Bounds() - if p(e.Event).In(r) { - continue - } - } - actTag.Handle(act, e) - } - ck() - case mus.CommitEvent: - cont = 0 - ck() - case mus.SnarfEvent, mus.InsertEvent: - actTag.Handle(act, e) - ck() - case mus.SelectEvent: - switch cont { - case scrollbar: - act.Clicksb(p(e.Event), 0) - wind.SendFirst(mus.Drain{}) - wind.Send(mus.DrainStop{}) - case sizer: - e.X += float32(act.Sp.X) - e.Y += float32(act.Sp.Y) - pt.X = int(e.X) - pt.Y = int(e.Y) - activate(p(e.Event), g) - sweepend() - case window: - actTag.Handle(act, e) - //q0, q1 := act.Dot() - if e.Button == 2 { - // s := string(act.Bytes()[q0:q1]) - // actTag.Handle(act, s) - // wind.Send(s) - } - } - cont = 0 - ck() - case key.Event: - actTag.Handle(act, e) - dirty = true - ck() - case event.Look: - alook(e) - ck() - case event.Cmd: - s := string(e.P) - switch s { - case "Put", "Get": - actTag.Handle(act, s) - ck() - case "New": - moveMouse(New(actCol, "", "").Loc().Min) - case "Newcol": - moveMouse(NewCol2(g, "").Loc().Min) - case "Del": - Del(actCol, actCol.ID(actTag)) - case "Sort": - aerr("Sort: TODO") - case "Delcol": - Delcol(g, g.ID(actCol)) - case "Exit": - aerr("Exit: TODO") - default: - if len(e.To) == 0 { - aerr("cmd has no destination: %q", s) + scroll(act, mus.ScrollEvent{Dy: 5, Event: e}) + case e := <-D.Mouse: + activate(p(e), g) + e = readmouse(e) + if down == 0 { + continue } - abs := AbsOf(e.Basedir, e.Name) - if strings.HasPrefix(s, "Edit ") { - s = s[5:] - // The event sink shouldn't be specified during - // compile time, but its the easiest way to - // see it works correctly with the editor - prog, err := edit.Compile(s, &edit.Options{Sender: wind, Origin: abs}) - if err != nil { - aerr(err.Error()) - continue - } - prog.Run(e.To[0]) - w := e.To[0].(*win.Win) - w.Resize(w.Size()) - //e.To[0].(*win.Win).Refresh() - ajump(e.To[0], false) - } else if strings.HasPrefix(s, "Install ") { - s = s[8:] - g.Install(actTag, s) + if borderHit(rel(e, act)) { + procBorderHit(e) } else { - x := strings.Fields(s) - if len(x) < 1 { - aerr("empty command") - continue - } - tagname := fmt.Sprintf("%s%c-%s", path.DirOf(abs), filepath.Separator, x[0]) - to := g.afinderr(path.DirOf(abs), tagname) - cmd(to.Body, path.DirOf(abs), s) - dirty = true + // assert("procButton", g) // + procButton(rel(e, act)) } - } - ck() - case edit.File: - g.acolor(e) - case edit.Print: - g.aout(string(e)) - case size.Event: - winSize = image.Pt(e.WidthPx, e.HeightPx) - g.Resize(winSize) - ck() - case paint.Event: - if !focused { - g.Resize(winSize) - } - if !lim.Allow() { - continue - } - g.Upload(wind) - wind.Publish() - case lifecycle.Event: - if e.To == lifecycle.StageDead { + repaint() + case <-sigterm: + logf("mouse: sigterm") return } - // NT doesn't repaint the window if another window covers it - if e.Crosses(lifecycle.StageFocused) == lifecycle.CrossOff { - focused = false - wind.Send(paint.Event{}) - } else if e.Crosses(lifecycle.StageFocused) == lifecycle.CrossOn { - focused = true - } - case error: - aerr(e.Error()) - case interface{}: - log.Printf("missing event: %#v\n", e) } - } - -} - -func cmd(f text.Editor, dir string, argv string) { - x := strings.Fields(argv) - if len(x) == 0 { - eprint("|: nothing on rhs") - return - } - n := x[0] - var a []string - if len(x) > 1 { - a = x[1:] - } - - cmd := exec.Command(n, a...) - cmd.Dir = dir - q0, q1 := f.Dot() - f.Delete(q0, q1) - q1 = q0 - var fd0 io.WriteCloser - fd1, err := cmd.StdoutPipe() - if err != nil { - panic(err) - } - fd2, err := cmd.StderrPipe() - if err != nil { - panic(err) - } - fd0, err = cmd.StdinPipe() - if err != nil { - panic(err) - } + }() - fd0.Close() - var wg sync.WaitGroup - donec := make(chan bool) - outc := make(chan []byte) - errc := make(chan []byte) - wg.Add(2) go func() { - defer wg.Done() - b := make([]byte, 65536) for { select { - case <-donec: + case e := <-D.Key: + kbdin(e, actTag, act) + repaint() + case <-sigterm: + logf("kbd: sigterm") return - default: - n, err := fd1.Read(b) - if err != nil { - if err == io.EOF { - break - } - eprint(err) - } - outc <- append([]byte{}, b[:n]...) } } }() - go func() { - defer wg.Done() - b := make([]byte, 65536) - for { - select { - case <-donec: - return - default: - n, err := fd2.Read(b) - if err != nil { - if err == io.EOF { - break - } +Loop: + for { + select { + case <-sigterm: + logf("mainselect: sigterm") + break Loop + case e := <-D.Size: + winSize = image.Pt(e.WidthPx, e.HeightPx) + g.Resize(winSize) + repaint() + case e := <-D.Paint: + if throttled() { + continue + } + if e.External { + g.Resize(winSize) + } + g.Upload() + wind.Publish() + case e := <-D.Lifecycle: + procLifeCycle(e) + repaint() + case e := <-events: + switch e := e.(type) { + case tag.GetEvent: + t := New(actCol, e.Basedir, e.Name) + if e.Addr != "" { + actTag = t.(*tag.Tag) + act = actTag.Body + //actTag.Handle(actTag.Body, edit.MustCompile(e.Addr)) + MoveMouse(act) + } else { + moveMouse(t.Loc().Min) } - errc <- append([]byte{}, b[:n]...) + case edit.File: + g.acolor(e) + case edit.Print: + g.aout(string(e)) + case error: + logf("unspecified error: %s", e) + case interface{}: + logf("missing event: %#v\n", e) + continue } + repaint() } - }() - cmd.Start() - go func() { - _, err = io.Copy(fd0, bytes.NewReader(append([]byte{}, f.Bytes()[q0:q1]...))) - if err != nil { - eprint(err) - return - } - cmd.Wait() - close(donec) - }() - go func() { - Loop: - for { - select { - case p := <-outc: - f.Insert(p, q1) - q1 += int64(len(p)) - case p := <-errc: - f.Insert(p, q1) - q1 += int64(len(p)) - case <-donec: - break Loop - } + } +} + +func teardown() { + select { + case clean := <-moribound: + if clean { + setLogFunc(log.Printf) + logf("TODO: polite shutdown") + close(sigterm) + close(moribound) } - }() + default: + } +} +func procLifeCycle(e lifecycle.Event) { + if e.To == lifecycle.StageDead { + teardown() + return + } + if e.Crosses(lifecycle.StageFocused) == lifecycle.CrossOff { + focused = false + } else if e.Crosses(lifecycle.StageFocused) == lifecycle.CrossOn { + focused = true + } } diff --git a/mouse.go b/mouse.go index abf79d0..8762c18 100644 --- a/mouse.go +++ b/mouse.go @@ -1,4 +1,180 @@ -// +build !linux -// +build !darwin - package main + +import ( + "image" + "time" + + "github.com/as/event" + "github.com/as/text" + "github.com/as/text/find" + "github.com/as/ui/tag" + "github.com/as/ui/win" + "golang.org/x/mobile/event/mouse" +) + +func Button(n uint) uint { + return 1 << n +} +func HasButton(n, mask uint) bool { + return Button(n)&mask != 0 +} + +var ( + last uint + lastpt image.Point + t0 = time.Now() +) + +func procButton(e mouse.Event) { + double := false + if last == down { + if time.Since(t0) < time.Second/2 && lastpt.In(image.Rect(-3, -3, 3, 3).Add(p(e))) { + double = true + } + } + t0 = time.Now() + last = down + lastpt = p(e) + t := actTag + w, _ := act.(*win.Win) + if w == nil { + return + } + + s0, s1 := w.Dot() + q0 := w.IndexOf(p(e)) + w.Origin() + q1 := q0 + act.Select(q0, q1) + repaint() + switch down { + case Button(1): + if double { + q0, q1 = find.FreeExpand(w, q0) + double = false + w.Select(q0, q1) + } else { + // In Acme and Sam, the double click action doesn't maintain + // a hold on the selection if the mouse is moved out of a rectangular + // region. I don't do the same thing here because it's sometimes + // advantageous to make the selection hold for a scrolling select + // operation. + q0, q1, e = sweepFunc(w, e, D.Mouse) + } + for down != 0 { + w.Select(q0, q1) + if HasButton(2, down) { + tag.Snarf(w, e) + q1 = q0 + } else if HasButton(3, down) { + q0, q1 = tag.Paste(w, e) + } + repaint() + e = rel(readmouse(<-D.Mouse), t) + } + t0 = time.Now() + w.Select(q0, q1) + case Button(2): + q0, q1, _ := sweepFunc(w, e, D.Mouse) + if q0 == q1 { + if text.Region3(q0, s0, s1) == 0 { + q0, q1 = s0, s1 + } else { + q0, q1 = find.ExpandFile(w.Bytes(), q0) + } + } + w.Select(s0, s1) + acmd(event.Cmd{ + Name: t.FileName(), + From: t, To: []event.Editor{w}, + Rec: event.Rec{Q0: q0, Q1: q0, P: w.Bytes()[q0:q1]}, + }) + case Button(3): + q0, q1, _ := sweepFunc(w, e, D.Mouse) + if q0 == q1 && text.Region3(q0, s0, s1) != 0 { + // Non-zero selection; so we want to look here explicitly + q0, q1 = find.ExpandFile(w.Bytes(), q0) + } + w.Select(s0, s1) // undo the sweep + g.Look(event.Look{ + Name: t.FileName(), + From: act, // The source can be the tag or the body + To: []event.Editor{actTag.Body}, // But the target is always the tag's body + Rec: event.Rec{ + Q0: q0, + Q1: q1, + P: act.Bytes()[q0:q1], + }, + }) + } +} + +// moveMouse(pt image.Point) // defined in mouse_other.go and mouse_linux.go +func MoveMouse(address interface{}) { + switch a := address.(type) { + case *win.Win: + p0, _ := a.Frame.Dot() + moveMouse(a.Loc().Min.Add(a.PointOf(p0))) + case image.Point: + moveMouse(a) + return + case int64: + w, _ := act.(*win.Win) + if w == nil { + return + } + p0, _ := w.Frame.Dot() + moveMouse(w.PointOf(p0)) + return + } +} + +func sweepFunc(w *win.Win, e mouse.Event, mc <-chan mouse.Event) (q0, q1 int64, e1 mouse.Event) { + start := down + q0, q1 = w.Dot() + w.Sq = q0 + for down == start { + w.Sq, q0, q1 = sweep(w, e, w.Sq, q0, q1) + w.Select(q0, q1) + repaint() + e = rel(readmouse(<-mc), w) + } + return q0, q1, e +} + +func cursorNop(p image.Point) {} + +func shouldCursor(p Plane) (fn func(image.Point)) { + switch p.(type) { + case Named: + return cursorNop + default: + return moveMouse + } +} +func ajump2(ed text.Editor, cursor bool) { + fn := moveMouse + if !cursor { + fn = nil + } + if ed, ok := ed.(text.Jumper); ok { + ed.Jump(fn) + } +} + +func ajump(p interface{}, cursor func(image.Point)) { + switch p := p.(type) { + case nil: + return //TODO(as): error message without a recursive call + case *tag.Tag: + if p != nil { + cursor(p.Loc().Min) + } + case text.Jumper: + p.Jump(cursor) + case Plane: + if cursor == nil { + cursor = shouldCursor(p) + } + cursor(p.Loc().Min) + } +} diff --git a/mouse_linux.go b/mouse_linux.go index 3c4f517..578f5bb 100644 --- a/mouse_linux.go +++ b/mouse_linux.go @@ -1,3 +1,5 @@ +// +build linux + package main import ( diff --git a/paint.go b/paint.go new file mode 100644 index 0000000..e6a474a --- /dev/null +++ b/paint.go @@ -0,0 +1,12 @@ +package main + +import ( + "golang.org/x/mobile/event/paint" +) + +func repaint() { + select { + case D.Paint <- paint.Event{}: + default: + } +} diff --git a/plane.go b/plane.go index 2807eb8..7ff145c 100644 --- a/plane.go +++ b/plane.go @@ -1,23 +1,7 @@ package main import ( - "image" + "github.com/as/ui/col" ) -type Plane interface { - Loc() image.Rectangle - Move(image.Point) - Resize(image.Point) - Refresh() -} - -func eq(a, b Plane) bool { - if a == nil || b == nil { - return false - } - return a.Loc() == b.Loc() -} - -func sizeof(r image.Rectangle) image.Point { - return r.Max.Sub(r.Min) -} +type Plane = col.Plane diff --git a/plumber.go b/plumber.go new file mode 100644 index 0000000..a62a0fa --- /dev/null +++ b/plumber.go @@ -0,0 +1,86 @@ +package main + +import ( + "os" + "os/exec" + "regexp" + "runtime" + "strings" +) + +type ( + PlumbAction func(msg *Plumbmsg) error + PlumbRule func(msg *Plumbmsg) PlumbAction +) + +type Plumbmsg struct { + src, dst string + wdir string + kind string + Attr + Data []byte +} +type Attr map[string]string + +func (p *Plumbmsg) Arg() string { + return strings.TrimSpace(string(p.Data)) +} + +var httpLink = NewRegexp("^http(s?)://", openBrowser) + +func NewRegexp(expr string, action PlumbAction) *regexpRule { + return ®expRule{regexp.MustCompile(expr), action} +} + +var openBrowser = PlumbAction(func(p *Plumbmsg) (err error) { + for _, browser := range browsers() { + full := append(strings.Fields(browser), p.Arg()) + cmd := exec.Command(full[0], full[1:]...) + err = cmd.Start() + if err == nil { + break + } + } + return err +}) + +type regexpRule struct { + *regexp.Regexp + action PlumbAction +} + +func (r *regexpRule) Plumb(msg *Plumbmsg) (matched bool, error error) { + if r.Match(msg.Data) { + return true, r.action(msg) + } + return false, nil +} + +func matches(match bool, err error) bool { + if match && err == nil { + return true + } + if err != nil { + logf("plumber: %s", err) + } + return match +} + +// browsers returns a list of commands to attempt for web visualization. +func browsers() []string { + cmds := []string{"chrome", "google-chrome", "firefox"} + switch runtime.GOOS { + case "darwin": + return append(cmds, "/usr/bin/open") + case "windows": + return append(cmds, "cmd /c start") + default: + userBrowser := os.Getenv("BROWSER") + if userBrowser != "" { + cmds = append([]string{userBrowser, "sensible-browser"}, cmds...) + } else { + cmds = append([]string{"sensible-browser"}, cmds...) + } + return append(cmds, "xdg-open") + } +} diff --git a/scroll.go b/scroll.go index e6c4e7a..c616f9d 100644 --- a/scroll.go +++ b/scroll.go @@ -2,14 +2,13 @@ package main import ( mus "github.com/as/text/mouse" - "github.com/as/ui/win" - "golang.org/x/mobile/event/paint" + "github.com/as/ui/tag" ) -func doScrollEvent(act *win.Win, e mus.ScrollEvent) { +func scroll(act tag.Window, e mus.ScrollEvent) { if e.Button == -1 { e.Dy = -e.Dy } actTag.Body.Scroll(e.Dy) - act.Window().Send(paint.Event{}) + repaint() } diff --git a/shape.go b/shape.go new file mode 100644 index 0000000..37ac4b0 --- /dev/null +++ b/shape.go @@ -0,0 +1,37 @@ +package main + +import "image" + +// Area returns the bounds in which the list elements can reside +func (c *Grid) Area() image.Rectangle { + dy := c.Tag.Loc().Dy() + r := c.Loc() + r.Min.Y += dy + return r +} + +// Minor returns the a point in Grid where Y is top-aligned +// and X is clamped between min.X and max.X +func (c *Grid) Minor(pt image.Point) image.Point { + pt.Y = c.Area().Min.Y + pt.X = clampx(pt.X, c.Area().Min.X, c.Area().Max.X) + return pt +} + +// Major returns the a point in Col where X is right-aligned +// and X is clamped between min.X and max.X +func (c *Grid) Major(pt image.Point) image.Point { + pt.Y = c.Area().Max.Y //-c.Area().Min.Y + pt.X = clampx(pt.X, c.Area().Min.X, c.Area().Max.X) //-c.Area().Min.X + return pt +} + +func clampx(v, l, h int) int { + if v < l { + return l + } + if v > h { + return h + } + return v +} diff --git a/sizers.go b/sizers.go index 88e6cd1..49afa15 100644 --- a/sizers.go +++ b/sizers.go @@ -1,11 +1,6 @@ package main func SizeThirdOf(size int) int { - return size - (size / 5 * 2) -} -func SizeSmall(size int) int { - if size-200 < 0 { - return 100 - } - return size - 200 + return size - size/2 + return size - (size / 10 * 6) } diff --git a/srv.go b/srv.go new file mode 100644 index 0000000..921affd --- /dev/null +++ b/srv.go @@ -0,0 +1,43 @@ +package main + +import ( + "github.com/as/srv/fs" +) + +var ( + srv *fs.Server + client *fs.Client + srverr, clienterr error +) + +func createnetworks() (fatal error) { + if *srvaddr != "" { + srv, srverr = fs.Serve("tcp", *srvaddr) + } + if *clientaddr != "" { + client, clienterr = fs.Dial("tcp", *clientaddr) + if clienterr != nil { + return clienterr + } + } + if srv != nil { + logf("listening for remote connections") + } + if client != nil { + logf("connecting to remote filesystem") + } + return nil +} + +func newfsclient() fs.Fs { + if client == nil { + return &fs.Local{} + } + if clienterr != nil { + client, clienterr = fs.Dial("tcp", *clientaddr) + } + if clienterr == nil { + return client + } + panic(clienterr) +} diff --git a/tile.go b/tile.go new file mode 100644 index 0000000..afd691e --- /dev/null +++ b/tile.go @@ -0,0 +1,32 @@ +package main + +import ( + "image" +) + +type Tile interface { + Delta(int) image.Point + Kid(n int) Plane + Len() int +} + +func fill(t Tile) { + if t.Len() == 0 { + return + } + for n := 0; n != t.Len(); n++ { + pt := t.Delta(n) + if pt == image.ZP { + panic("zp") + } + k := t.Kid(n) + k.Resize(pt) + } +} + +func identity(x, y int) image.Point { + if x == 0 || y == 0 { + return image.ZP + } + return image.Pt(x, y) +} diff --git a/util.go b/util.go new file mode 100644 index 0000000..274dcba --- /dev/null +++ b/util.go @@ -0,0 +1,100 @@ +package main + +import ( + "image" + + "github.com/as/ui/tag" + "github.com/as/ui/win" + "golang.org/x/mobile/event/mouse" +) + +const ( + scrollX = 10 + sbWidth = 10 +) + +var ( + tagHeight = tag.Height(*ftsize) + sizerR = image.Rect(0, 0, scrollX, tagHeight) +) + +func rel(e mouse.Event, p Plane) mouse.Event { + pt := p.Loc().Min + e.X -= float32(pt.X) + e.Y -= float32(pt.Y) + return e +} + +func absP(e mouse.Event, sp image.Point) image.Point { + return p(e).Add(sp) +} + +func canopy(pt image.Point) bool { + r := g.Loc() + r.Max.Y = r.Min.Y + g.Tag.Loc().Dy()*2 + return pt.In(r) +} + +func p(e mouse.Event) image.Point { + return image.Pt(int(e.X), int(e.Y)) +} + +func inSizer(pt image.Point) bool { + return pt.In(sizerR) +} + +func inScroll(pt image.Point) bool { + return pt.X >= 0 && pt.X < sbWidth +} + +func yRegion(y, ymin, ymax int) int { + if y < ymin { + return 1 + } + if y > ymax { + return -1 + } + return 0 +} + +func clamp(v, l, h int64) int64 { + if v < l { + return l + } + if v > h { + return h + } + return v +} + +func sweep(w *win.Win, e mouse.Event, s, q0, q1 int64) (int64, int64, int64) { + r := image.Rectangle{image.ZP, w.Size()} + y := int(e.Y) + padY := tagHeight + lo := r.Min.Y + padY + hi := r.Dy() - padY + units := w.Bounds().Dy() + if units == 0 { + units++ + } + reg := yRegion(y, lo, hi) + + if reg != 0 { + if reg == 1 { + w.Scroll(-((lo-y)%units + 1) * 3) + } else { + w.Scroll(+((y-hi)%units + 1) * 3) + } + } + q := w.IndexOf(image.Pt(int(e.X), int(e.Y))) + w.Origin() + if q0 == s { + if q < q0 { + return q0, q, q0 + } + return q0, q0, q + } + if q > q1 { + return q1, q1, q + } + return q1, q, q1 +}