summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--actions.go4
-rw-r--r--command.go4
-rw-r--r--handlers.go52
-rw-r--r--main.go30
-rw-r--r--state.go14
-rw-r--r--tls.go3
-rw-r--r--tui.go161
7 files changed, 194 insertions, 74 deletions
diff --git a/actions.go b/actions.go
index c80445b..656ab99 100644
--- a/actions.go
+++ b/actions.go
@@ -252,7 +252,7 @@ func fetch(state *BrowserState, u string, tlsConf *tls.Config) (*sliderule.Respo
var tofuErr *TOFUViolation
if errors.As(err, &tofuErr) {
- writeError(err.Error())
+ state.Printer.PrintError(err.Error())
state.Readline.SetPrompt("Trust new certificate instead (y/n)? [n] ")
line, err := state.Readline.Readline()
if err != nil {
@@ -284,7 +284,7 @@ func upload(state *BrowserState, u string, body io.Reader, tlsConf *tls.Config)
response, err := sliderule.NewClient(tlsConf).Upload(ctx, u, body)
var tofuErr *TOFUViolation
if errors.As(err, &tofuErr) {
- writeError(err.Error())
+ state.Printer.PrintError(err.Error())
state.Readline.SetPrompt("Trust new certificate instead (y/n)? [n] ")
line, err := state.Readline.Readline()
if err != nil {
diff --git a/command.go b/command.go
index e231c7a..1dfe13f 100644
--- a/command.go
+++ b/command.go
@@ -20,7 +20,7 @@ type Command struct {
func ParseCommand(line string) (*Command, error) {
line = strings.TrimSpace(line)
if line == "" {
- return &Command{Name: "print"}, nil
+ return &Command{Name: "default"}, nil
}
cmd, rest, _ := strings.Cut(line, " ")
@@ -404,6 +404,8 @@ func RunCommand(cmd *Command, state *BrowserState) error {
return Outline(state)
case "pipe":
return Pipe(state, cmd.Args[0])
+ case "default":
+ return HandleResource(state)
case "print":
return Print(state)
case "links":
diff --git a/handlers.go b/handlers.go
index 599d53b..c87d82b 100644
--- a/handlers.go
+++ b/handlers.go
@@ -8,6 +8,7 @@ import (
"net/url"
"strings"
+ "github.com/charmbracelet/lipgloss"
"tildegit.org/tjp/sliderule"
"tildegit.org/tjp/sliderule/gemini"
"tildegit.org/tjp/sliderule/gemini/gemtext"
@@ -121,7 +122,7 @@ func parseGophermapDoc(body []byte, softWrap int) (string, []Link, error) {
Text: item.Display,
Target: fmtGopherURL(item.Type, item.Selector, item.Hostname, item.Port),
})
- if _, err := b.WriteString(fmt.Sprintf("[%d]%s %s%s%s\n", i, padding(i, width), linkStyle, item.Display, ansiClear)); err != nil {
+ if _, err := b.WriteString(fmt.Sprintf("[%d]%s %s\n", i, padding(i, width), linkStyle.Render(item.Display))); err != nil {
return "", nil, err
}
i += 1
@@ -131,16 +132,15 @@ func parseGophermapDoc(body []byte, softWrap int) (string, []Link, error) {
return b.String(), l, nil
}
-const (
- ansiClear = "\x1b[0m"
- linkStyle = "\x1b[38;5;33m"
- promptStyle = "\x1b[38;5;39m"
- quoteStyle = "\x1b[38;5;208m\x1b[3m"
- rawStyle = "\x1b[38;5;249m"
- h1Style = "\x1b[38;5;154m\x1b[1m\x1b[4m"
- h2Style = "\x1b[38;5;50m\x1b[4m"
- h3Style = "\x1b[38;5;6m\x1b[4m"
- listStyle = "\x1b[38;5;3m"
+var (
+ linkStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("33"))
+ promptStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("39"))
+ quoteStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("208")).Italic(true)
+ rawStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("249"))
+ h1Style = lipgloss.NewStyle().Foreground(lipgloss.Color("154")).Bold(true).Underline(true)
+ h2Style = lipgloss.NewStyle().Foreground(lipgloss.Color("50")).Underline(true)
+ h3Style = lipgloss.NewStyle().Foreground(lipgloss.Color("6")).Underline(true)
+ listStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("3"))
)
func parseGemtextDoc(body []byte, softWrap int) (string, []Link, error) {
@@ -183,22 +183,30 @@ func parseGemtextDoc(body []byte, softWrap int) (string, []Link, error) {
if len(label) == 0 {
label = ll.URL()
}
- if _, err := b.WriteString(fmt.Sprintf("[%d]%s %s%s%s\n", i, padding(i, width), linkStyle, label, ansiClear)); err != nil {
- return "", nil, err
+ for j, line := range fold(label, softWrap) {
+ var prefix string
+ if j == 0 {
+ prefix = fmt.Sprintf("[%d]%s ", i, padding(i, width))
+ } else {
+ prefix = strings.Repeat(" ", width+3)
+ }
+ if _, err := fmt.Fprintf(&b, "%s%s\n", prefix, linkStyle.Render(line)); err != nil {
+ return "", nil, err
+ }
}
i += 1
case gemtext.LineTypeQuote:
q := item.(gemtext.QuoteLine)
for _, line := range fold(q.Body(), softWrap-1) {
line = strings.TrimSpace(line)
- if _, err := b.WriteString(textpad + "> " + quoteStyle + line + ansiClear + "\n"); err != nil {
+ if _, err := b.WriteString(textpad + "> " + quoteStyle.Render(line) + "\n"); err != nil {
return "", nil, err
}
}
case gemtext.LineTypePreformatToggle:
case gemtext.LineTypePreformattedText:
for _, line := range fold(item.String(), softWrap) {
- if _, err := b.WriteString(textpad + rawStyle + line + ansiClear + "\n"); err != nil {
+ if _, err := b.WriteString(textpad + rawStyle.Render(line) + "\n"); err != nil {
return "", nil, err
}
}
@@ -210,19 +218,19 @@ func parseGemtextDoc(body []byte, softWrap int) (string, []Link, error) {
fallthrough
case gemtext.LineTypeHeading1:
hLevel += 1
- var color string
+ var style lipgloss.Style
switch hLevel {
case 1:
- color = h1Style
+ style = h1Style
case 2:
- color = h2Style
+ style = h2Style
case 3:
- color = h3Style
+ style = h3Style
}
for _, line := range fold(item.String(), softWrap) {
line = strings.TrimRight(line, "\r\n")
- if _, err := b.WriteString(textpad + color + line + ansiClear + "\n"); err != nil {
+ if _, err := b.WriteString(textpad + style.Render(line) + "\n"); err != nil {
return "", nil, err
}
}
@@ -233,7 +241,7 @@ func parseGemtextDoc(body []byte, softWrap int) (string, []Link, error) {
if i == 0 {
lpad = "* "
}
- if _, err := b.WriteString(textpad + listStyle + lpad + line + ansiClear + "\n"); err != nil {
+ if _, err := b.WriteString(textpad + lpad + listStyle.Render(line) + "\n"); err != nil {
return "", nil, err
}
}
@@ -307,5 +315,5 @@ func numberWidth(i int) int {
}
func padding(num int, width int) string {
- return string(bytes.Repeat([]byte{' '}, width-numberWidth(num)))
+ return strings.Repeat(" ", width-numberWidth(num))
}
diff --git a/main.go b/main.go
index 695a8fc..715bc57 100644
--- a/main.go
+++ b/main.go
@@ -2,19 +2,18 @@ package main
import (
"flag"
- "fmt"
"io"
"log"
- "os"
"strings"
"github.com/chzyer/readline"
)
var (
- cmdMode = flag.String("c", "", "")
- helpMode = flag.Bool("h", false, "")
- quietMode = flag.Bool("q", false, "")
+ cmdMode = flag.String("c", "", "")
+ helpMode = flag.Bool("h", false, "")
+ quietMode = flag.Bool("q", false, "")
+ promptMode = flag.Bool("p", false, "")
)
func main() {
@@ -44,8 +43,11 @@ func main() {
state.Quiet = true
}
- runInteractivePrompt(state, flag.Args())
- // runTUI(state, flag.Args())
+ if *promptMode {
+ runInteractivePrompt(state, flag.Args())
+ } else {
+ runTUI(state, flag.Args())
+ }
}
func buildReadline(prompt string, conf *Config) (*readline.Instance, error) {
@@ -91,7 +93,7 @@ func buildInitialState() (*BrowserState, error) {
}
state.Identities = idents
- rl, err := buildReadline(Prompt, conf)
+ rl, err := buildReadline(prompt(), conf)
if err != nil {
log.Fatal(err)
}
@@ -105,12 +107,12 @@ func runInteractivePrompt(state *BrowserState, args []string) {
if len(args) > 0 {
if err := Go(state, args[0]); err != nil {
- writeError(err.Error())
+ state.Printer.PrintError(err.Error())
}
}
for {
- state.Readline.SetPrompt(Prompt)
+ state.Readline.SetPrompt(prompt())
line, err := state.Readline.Readline()
if err == io.EOF {
break
@@ -120,7 +122,7 @@ func runInteractivePrompt(state *BrowserState, args []string) {
}
if err := handleCmdLine(state, line); err != nil {
- writeError(err.Error())
+ state.Printer.PrintError(err.Error())
}
}
}
@@ -136,8 +138,10 @@ func handleCmdLine(state *BrowserState, line string) error {
return nil
}
-const Prompt = promptStyle + "X-1" + ansiClear + "> "
+func prompt() string {
+ return promptStyle.Render("X-1") + "> "
+}
func writeError(msg string) {
- fmt.Fprintf(os.Stdout, "\x1b[31m%s\x1b[0m\n", msg)
+ _ = PromptPrinter{}.PrintError(msg)
}
diff --git a/state.go b/state.go
index 918346f..5c3aa5c 100644
--- a/state.go
+++ b/state.go
@@ -3,10 +3,12 @@ package main
import (
"bytes"
"errors"
+ "fmt"
"net/url"
"os"
"os/exec"
+ "github.com/charmbracelet/lipgloss"
"github.com/chzyer/readline"
)
@@ -69,16 +71,17 @@ func NewBrowserState(conf *Config) *BrowserState {
type Printer interface {
PrintModal(*BrowserState, []byte) error
PrintPage(*BrowserState, string) error
+ PrintError(string) error
}
type PromptPrinter struct{}
-func (_ PromptPrinter) PrintModal(state *BrowserState, contents []byte) error {
+func (PromptPrinter) PrintModal(state *BrowserState, contents []byte) error {
_, err := os.Stdout.Write(contents)
return err
}
-func (_ PromptPrinter) PrintPage(state *BrowserState, body string) error {
+func (PromptPrinter) PrintPage(state *BrowserState, body string) error {
if state.Quiet {
return nil
}
@@ -105,3 +108,10 @@ func (_ PromptPrinter) PrintPage(state *BrowserState, body string) error {
return errors.New("invalid 'pager' value in configuration")
}
}
+
+var promptErrorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("196")).Bold(true)
+
+func (PromptPrinter) PrintError(msg string) error {
+ _, err := fmt.Println(promptErrorStyle.Render(msg))
+ return err
+}
diff --git a/tls.go b/tls.go
index d4452f2..b30a91b 100644
--- a/tls.go
+++ b/tls.go
@@ -99,8 +99,7 @@ func createIdentity(state *BrowserState, name string) (*tls.Config, error) {
}
}
- snLimit := new(big.Int).Lsh(big.NewInt(1), 128)
- serialNumber, err := rand.Int(rand.Reader, snLimit)
+ serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
if err != nil {
return nil, err
}
diff --git a/tui.go b/tui.go
index c131110..48bc2d9 100644
--- a/tui.go
+++ b/tui.go
@@ -1,80 +1,177 @@
package main
import (
+ "fmt"
"os"
+ "strings"
- tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/bubbles/textinput"
"github.com/charmbracelet/bubbles/viewport"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
)
-type TUIModel struct {
- State *BrowserState
+func runTUI(state *BrowserState, args []string) {
+ model := NewMainModel(state)
+ state.Printer = (*TUIPrinter)(model)
+
+ if len(args) > 0 {
+ if err := Go(state, args[0]); err != nil {
+ state.Printer.PrintError(err.Error())
+ }
+ }
+
+ p := tea.NewProgram(model, tea.WithAltScreen())
+ if _, err := p.Run(); err != nil {
+ state.Printer.PrintError(err.Error())
+ os.Exit(1)
+ }
+}
+
+var (
+ hdrStyle = lipgloss.NewStyle().Background(lipgloss.Color("56")).Bold(true)
+ ftrStyle = lipgloss.NewStyle().Background(lipgloss.Color("56"))
+ errStyle = lipgloss.NewStyle().Background(lipgloss.Color("196")).Bold(true)
+)
+
+type MainModel struct {
+ State *BrowserState
Viewport viewport.Model
- inited bool
+ Prompt *textinput.Model
+ ErrorMsg string
}
-func NewTUIModel(state *BrowserState) *TUIModel {
- return &TUIModel{State: state}
+func NewMainModel(state *BrowserState) *MainModel {
+ return &MainModel{
+ State: state,
+ Viewport: viewport.Model{
+ HighPerformanceRendering: true,
+ YPosition: 2,
+ },
+ }
}
-func (model *TUIModel) Init() tea.Cmd {
+func (model *MainModel) Init() tea.Cmd {
return nil
}
-func (model *TUIModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+func (model *MainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ if model.Prompt != nil {
+ return model.updatePrompt(msg)
+ }
+
+ m := model
+ var cmds []tea.Cmd
+
switch msg := msg.(type) {
case tea.KeyMsg:
+ model.ErrorMsg = ""
+
switch msg.String() {
case "ctrl+c", "q":
- return model, tea.Quit
+ cmds = append(cmds, tea.Quit)
+ case "ctrl+l":
+ cmds = append(cmds, viewport.Sync(model.Viewport))
case "g":
- model.Viewport.GotoTop()
- return model, nil
+ lines := model.Viewport.GotoTop()
+ cmds = append(cmds, viewport.ViewUp(model.Viewport, lines))
case "G":
- model.Viewport.GotoBottom()
- return model, nil
+ lines := model.Viewport.GotoBottom()
+ cmds = append(cmds, viewport.ViewDown(model.Viewport, lines))
+ case ":":
+ p := textinput.New()
+ model.Prompt = &p
+ cmds = append(cmds, p.Focus())
}
case tea.WindowSizeMsg:
- model.inited = true
model.Viewport.Width = msg.Width
- model.Viewport.Height = msg.Height - 1
+ model.Viewport.Height = msg.Height - 2
+ hdrStyle = hdrStyle.Width(msg.Width)
+ cmds = append(cmds, viewport.Sync(model.Viewport))
}
var cmd tea.Cmd
- model.Viewport, cmd = model.Viewport.Update(msg)
+ vp, cmd := model.Viewport.Update(msg)
+ model.Viewport = vp
+ cmds = append(cmds, cmd)
+ return m, tea.Batch(cmds...)
+}
+
+func (model *MainModel) updatePrompt(msg tea.Msg) (tea.Model, tea.Cmd) {
+ if keymsg, ok := msg.(tea.KeyMsg); ok {
+ model.ErrorMsg = ""
+
+ switch keymsg.String() {
+ case "enter":
+ cmd, err := ParseCommand(model.Prompt.Value())
+ model.Prompt = nil
+ if err != nil {
+ model.State.Printer.PrintError(err.Error())
+ return model, nil
+ }
+ if err := RunCommand(cmd, model.State); err != nil {
+ model.State.Printer.PrintError(err.Error())
+ return model, nil
+ }
+ return model, viewport.Sync(model.Viewport)
+ }
+ }
+
+ p, cmd := model.Prompt.Update(msg)
+ model.Prompt = &p
return model, cmd
}
-func (model *TUIModel) View() string {
- return model.Viewport.View()
+func (model *MainModel) View() string {
+ return model.viewHeader() + "\n" + model.Viewport.View() + "\n" + model.viewFooter()
}
-func runTUI(state *BrowserState, args []string) {
- model := NewTUIModel(state)
- state.Printer = (*TUIPrinter)(model)
+func (model *MainModel) viewHeader() string {
+ hdrLine := " Welcome to X-1"
+ if model.State.Url != nil {
+ hdrLine = " " + model.State.Url.String()
+ }
+ return hdrStyle.Render(hdrLine)
+}
- if len(args) > 0 {
- if err := Go(state, args[0]); err != nil {
- writeError(err.Error())
- }
+func (model *MainModel) viewFooter() string {
+ if model.ErrorMsg != "" {
+ return errStyle.Render(model.ErrorMsg)
}
- p := tea.NewProgram(model, tea.WithAltScreen())
- if _, err := p.Run(); err != nil {
- writeError(err.Error())
- os.Exit(1)
+ var footerLine string
+ if model.Prompt != nil {
+ footerLine = ftrStyle.Width(hdrStyle.GetWidth()).Render(model.Prompt.View())
+ } else {
+ footerLine = ftrStyle.Render(" ") + ftrStyle.Copy().Italic(true).Render(model.State.DocType)
+ pct := ftrStyle.Render(fmt.Sprintf("%3.f%% ", model.Viewport.ScrollPercent()*100))
+ footerLine += ftrStyle.Render(strings.Repeat(" ", max(0, model.Viewport.Width-lipgloss.Width(footerLine)-lipgloss.Width(pct))))
+ footerLine += pct
}
+ return footerLine
}
-type TUIPrinter TUIModel
+func max(a, b int) int {
+ if a < b {
+ return b
+ }
+ return a
+}
+
+type TUIPrinter MainModel
func (p *TUIPrinter) PrintModal(state *BrowserState, contents []byte) error {
- (*TUIModel)(p).Viewport.SetContent(string(contents))
+ (*MainModel)(p).Viewport.SetContent(strings.TrimSuffix(string(contents), "\n"))
return nil
}
func (p *TUIPrinter) PrintPage(state *BrowserState, body string) error {
- (*TUIModel)(p).Viewport.SetContent(body)
+ (*MainModel)(p).Viewport.SetContent(strings.TrimSuffix(body, "\n"))
+ return nil
+}
+
+func (p *TUIPrinter) PrintError(msg string) error {
+ (*MainModel)(p).ErrorMsg = msg
return nil
}