~adnano/astronaut

58d8db93861352aee7f8138e65f733144b12289a — Adnan Maolood 1 year, 11 months ago d06221a
browser: Replace find mode with find command
4 files changed, 269 insertions(+), 206 deletions(-)

M browser.go
M command.go
M config/astronaut.conf
M text.go
M browser.go => browser.go +23 -38
@@ 30,7 30,6 @@ const (
	ModeNormal Mode = iota
	ModeCommand
	ModeInput
	ModeFind
	ModeSave
	ModeFollow
)


@@ 191,7 190,7 @@ func (b *Browser) Draw() {
			b.statusView.DrawText(0, 0, b.message.text, style)
		}

	case ModeCommand, ModeInput, ModeFind, ModeSave:
	case ModeCommand, ModeInput, ModeSave:
		b.input.Draw()

	case ModeFollow:


@@ 267,6 266,9 @@ func (b *Browser) Event(event tcell.Event) {
				return
			}
			b.RunCommand(strings.Fields(event.Text())...)

		case *ui.EventTextEdit:
			b.PreviewCommand(strings.Fields(event.Text())...)
		}

	case ModeInput:


@@ 286,31 288,6 @@ func (b *Browser) Event(event tcell.Event) {
			tab.DoBackground(req)
		}

	case ModeFind:
		switch event := event.(type) {
		case *ui.EventTextInput:
			b.mode = ModeNormal
			if event.NoInput() {
				return
			}
			text := tab.Text()
			if text == nil {
				return
			}
			if err := text.Find(event.Text()); err != nil {
				b.Error(err)
				return
			}
			b.view.Invalidate()

		case *ui.EventTextEdit:
			text := tab.Text()
			if text == nil {
				return
			}
			text.Find(event.Text())
		}

	case ModeSave:
		switch event := event.(type) {
		case *ui.EventTextInput:


@@ 619,12 596,26 @@ func (b *Browser) RunCommand(args ...string) {
	}
	b.clearMessage()
	c, ok := commands[args[0]]
	if ok {
		if err := c(b, args[1:]...); err != nil {
			b.Error(err)
		}
	} else {
	if !ok {
		b.Error(fmt.Errorf("%q is not a command", args[0]))
		return
	}
	if err := c.Func(b, args[1:]...); err != nil {
		b.Error(err)
	}
}

func (b *Browser) PreviewCommand(args ...string) {
	if len(args) == 0 {
		return
	}
	b.clearMessage()
	c, ok := commands[args[0]]
	if !ok || !c.Preview {
		return
	}
	if err := c.Func(b, args[1:]...); err != nil {
		b.Error(err)
	}
}



@@ 691,12 682,6 @@ func (b *Browser) InputMode(prompt string, sensitive bool) {
	b.input.Sensitive(sensitive)
}

func (b *Browser) FindMode() {
	b.mode = ModeFind
	b.input.Prompt("/")
	b.view.Invalidate()
}

func (b *Browser) SaveMode(data io.ReadCloser, name string) {
	b.mu.Lock()
	defer b.mu.Unlock()

M command.go => command.go +245 -165
@@ 11,189 11,269 @@ import (
	"git.sr.ht/~adnano/go-gemini"
)

type Command func(b *Browser, args ...string) error
type Command struct {
	Func    func(b *Browser, args ...string) error
	Preview bool
}

var commands = map[string]Command{
	"prompt": func(b *Browser, args ...string) error {
		b.CommandMode()
		prompt := strings.Join(args, " ")
		b.input.SetInput(prompt)
		return nil
	"prompt": Command{
		Func: cmdPrompt,
	},
	"find": func(b *Browser, args ...string) error {
		if len(args) == 0 {
			b.FindMode()
			return nil
		}

		switch args[0] {
		case "prev":
			tab := b.tabs[b.tab]
			text := tab.Text()
			if text == nil {
				return nil
			}
			text.FindPrev()
		case "next":
			tab := b.tabs[b.tab]
			text := tab.Text()
			if text == nil {
				return nil
			}
			text.FindNext()
		default:
			return errors.New("usage: find [prev|next]")
		}
		return nil
	"find": Command{
		Func:    cmdFind,
		Preview: true,
	},
	"follow": func(b *Browser, args ...string) error {
		return b.FollowMode()
	"follow": Command{
		Func: cmdFollow,
	},
	"jump": func(b *Browser, args ...string) error {
		b.Jump()
		return nil
	"jump": Command{
		Func: cmdJump,
	},
	"open": func(b *Browser, args ...string) error {
		if len(args) == 0 {
			return errors.New("usage: open <url>")
		}

		rawurl := args[0]
		u, err := url.Parse(rawurl)
		if err != nil {
			return err
		}

		if len(u.Scheme) == 0 && len(u.Host) == 0 {
			u2, err := url.Parse("gemini://" + rawurl)
			if err == nil {
				u = u2
			}
		}
		b.tabs[b.tab].DoBackground(&gemini.Request{URL: u})
		return nil
	"open": Command{
		Func: cmdOpen,
	},
	"close": func(b *Browser, args ...string) error {
		b.CloseTab(b.tab)
		return nil
	"close": Command{
		Func: cmdClose,
	},
	"tab": func(b *Browser, args ...string) error {
		if len(args) == 0 {
			return errors.New("usage: tab prev|next")
		}
		switch args[0] {
		case "prev":
			b.Previous()
		case "next":
			b.Next()
		}
		return nil
	"tab": Command{
		Func: cmdTab,
	},
	"newtab": func(b *Browser, args ...string) error {
		tab := b.NewTab()
		tab.Do(&gemini.Request{
			URL: &url.URL{
				Scheme: "about",
				Host:   "newtab",
			},
		})
		b.makeTabVisible()
		return nil
	"newtab": Command{
		Func: cmdNewtab,
	},
	"clone": func(b *Browser, args ...string) error {
		tab := b.tabs[b.tab]
		clone := b.NewTab()
		clone.Clone(tab)
		clone.Reload()
		b.makeTabVisible()
		return nil
	"clone": Command{
		Func: cmdClone,
	},
	"reload": func(b *Browser, args ...string) error {
		b.tabs[b.tab].Reload()
		return nil
	"reload": Command{
		Func: cmdReload,
	},
	"cancel": func(b *Browser, args ...string) error {
		b.tabs[b.tab].Cancel()
		return nil
	"cancel": Command{
		Func: cmdCancel,
	},
	"back": func(b *Browser, args ...string) error {
		tab := b.tabs[b.tab]
		if !tab.Back() {
			b.Message("Already at beginning of history")
		}
		return nil
	"back": Command{
		Func: cmdBack,
	},
	"forward": func(b *Browser, args ...string) error {
		tab := b.tabs[b.tab]
		if !tab.Forward() {
			b.Message("Already at end of history")
		}
		return nil
	"forward": Command{
		Func: cmdForward,
	},
	"quit": func(b *Browser, args ...string) error {
		b.view.Exit()
		return nil
	"quit": Command{
		Func: cmdQuit,
	},
	"scroll": func(b *Browser, args ...string) error {
		const usage = "usage: scroll <n>[%]"
		if len(args) == 0 {
			return errors.New(usage)
		}
		amount := args[0]
		switch amount {
		case "top":
			b.tabView.Top()
			b.tabView.Invalidate()
		case "bottom":
			b.tabView.Bottom()
			b.tabView.Invalidate()
		default:
			var scroll int
			if strings.HasSuffix(amount, "%") {
				percent, err := strconv.Atoi(amount[:len(amount)-1])
				if err != nil {
					return errors.New(usage)
				}
				_, h := b.tabView.Size()
				scroll = h * percent / 100
			} else {
				var err error
				scroll, err = strconv.Atoi(amount)
				if err != nil {
					return errors.New(usage)
				}
			}
			b.tabView.ScrollDown(scroll)
			b.tabView.Invalidate()
		}
		return nil
	"scroll": Command{
		Func: cmdScroll,
	},
	"save": func(b *Browser, args ...string) error {
		tab := b.tabs[b.tab]
		name := path.Base(tab.URL())
		b.saveMode(io.NopCloser(&tab.buf), name)
		return nil
	"save": Command{
		Func: cmdSave,
	},
	"bookmark": func(b *Browser, args ...string) error {
		tab := b.tabs[b.tab]
		if b.bookmarks.Add(gemini.LineLink{
			URL:  tab.URL(),
			Name: tab.Title(),
		}) {
			b.Message("Bookmarked")
		} else {
			b.Message("Already bookmarked")
		}
		return nil
	"bookmark": Command{
		Func: cmdBookmark,
	},
	"search": func(b *Browser, args ...string) error {
		if len(args) == 0 {
			return errors.New("usage: search <query...>")
		}
		u := b.config.Search.URL.ResolveReference(&url.URL{
			RawQuery: gemini.QueryEscape(strings.Join(args, " ")),
		})
		b.tabs[b.tab].DoBackground(&gemini.Request{URL: u})
		return nil
	"search": Command{
		Func: cmdSearch,
	},
}

func cmdPrompt(b *Browser, args ...string) error {
	b.CommandMode()
	prompt := strings.Join(args, " ")
	b.input.SetInput(prompt)
	return nil
}

func cmdFind(b *Browser, args ...string) error {
	tab := b.tabs[b.tab]
	text := tab.Text()
	if text == nil {
		return nil
	}

	if len(args) == 0 {
		return nil
	}

	// TODO: Dedicated commands for prev/next?
	switch args[0] {
	case "prev":
		text.FindPrev()
		b.view.Invalidate()
		return nil
	case "next":
		text.FindNext()
		b.view.Invalidate()
		return nil
	default:
		// return errors.New("usage: find [prev|next]")
	}

	text.Find(strings.Join(args, " "))
	b.view.Invalidate()

	return nil
}

func cmdFollow(b *Browser, args ...string) error {
	return b.FollowMode()
}

func cmdJump(b *Browser, args ...string) error {
	b.Jump()
	return nil
}

func cmdOpen(b *Browser, args ...string) error {
	if len(args) == 0 {
		return errors.New("usage: open <url>")
	}

	rawurl := args[0]
	u, err := url.Parse(rawurl)
	if err != nil {
		return err
	}

	if len(u.Scheme) == 0 && len(u.Host) == 0 {
		u2, err := url.Parse("gemini://" + rawurl)
		if err == nil {
			u = u2
		}
	}
	b.tabs[b.tab].DoBackground(&gemini.Request{URL: u})
	return nil
}

func cmdClose(b *Browser, args ...string) error {
	b.CloseTab(b.tab)
	return nil
}

func cmdTab(b *Browser, args ...string) error {
	if len(args) == 0 {
		return errors.New("usage: tab prev|next")
	}
	switch args[0] {
	case "prev":
		b.Previous()
	case "next":
		b.Next()
	}
	return nil
}

func cmdNewtab(b *Browser, args ...string) error {
	tab := b.NewTab()
	tab.Do(&gemini.Request{
		URL: &url.URL{
			Scheme: "about",
			Host:   "newtab",
		},
	})
	b.makeTabVisible()
	return nil
}

func cmdClone(b *Browser, args ...string) error {
	tab := b.tabs[b.tab]
	clone := b.NewTab()
	clone.Clone(tab)
	clone.Reload()
	b.makeTabVisible()
	return nil
}

func cmdReload(b *Browser, args ...string) error {
	b.tabs[b.tab].Reload()
	return nil
}

func cmdCancel(b *Browser, args ...string) error {
	b.tabs[b.tab].Cancel()
	return nil
}

func cmdBack(b *Browser, args ...string) error {
	tab := b.tabs[b.tab]
	if !tab.Back() {
		b.Message("Already at beginning of history")
	}
	return nil
}

func cmdForward(b *Browser, args ...string) error {
	tab := b.tabs[b.tab]
	if !tab.Forward() {
		b.Message("Already at end of history")
	}
	return nil
}

func cmdQuit(b *Browser, args ...string) error {
	b.view.Exit()
	return nil
}

func cmdScroll(b *Browser, args ...string) error {
	const usage = "usage: scroll <n>[%]"
	if len(args) == 0 {
		return errors.New(usage)
	}
	amount := args[0]
	switch amount {
	case "top":
		b.tabView.Top()
		b.tabView.Invalidate()
	case "bottom":
		b.tabView.Bottom()
		b.tabView.Invalidate()
	default:
		var scroll int
		if strings.HasSuffix(amount, "%") {
			percent, err := strconv.Atoi(amount[:len(amount)-1])
			if err != nil {
				return errors.New(usage)
			}
			_, h := b.tabView.Size()
			scroll = h * percent / 100
		} else {
			var err error
			scroll, err = strconv.Atoi(amount)
			if err != nil {
				return errors.New(usage)
			}
		}
		b.tabView.ScrollDown(scroll)
		b.tabView.Invalidate()
	}
	return nil
}

func cmdSave(b *Browser, args ...string) error {
	tab := b.tabs[b.tab]
	name := path.Base(tab.URL())
	b.saveMode(io.NopCloser(&tab.buf), name)
	return nil
}

func cmdBookmark(b *Browser, args ...string) error {
	tab := b.tabs[b.tab]
	if b.bookmarks.Add(gemini.LineLink{
		URL:  tab.URL(),
		Name: tab.Title(),
	}) {
		b.Message("Bookmarked")
	} else {
		b.Message("Already bookmarked")
	}
	return nil
}

func cmdSearch(b *Browser, args ...string) error {
	if len(args) == 0 {
		return errors.New("usage: search <query...>")
	}
	u := b.config.Search.URL.ResolveReference(&url.URL{
		RawQuery: gemini.QueryEscape(strings.Join(args, " ")),
	})
	b.tabs[b.tab].DoBackground(&gemini.Request{URL: u})
	return nil
}

M config/astronaut.conf => config/astronaut.conf +1 -1
@@ 12,7 12,7 @@ follow {
}

bind : prompt
bind / find
bind / prompt "find "
bind N find prev
bind n find next
bind f follow

M text.go => text.go +0 -2
@@ 461,7 461,6 @@ func (t *Text) FindPrev() {
		y++
	}
	t.view.MakeVisible(0, y)
	t.view.Invalidate()
}

func (t *Text) FindNext() {


@@ 482,5 481,4 @@ func (t *Text) FindNext() {
		y++
	}
	t.view.MakeVisible(0, y)
	t.view.Invalidate()
}