mirror of
https://github.com/boolean-maybe/tiki
synced 2026-04-21 13:37:20 +00:00
261 lines
7.6 KiB
Go
261 lines
7.6 KiB
Go
package view
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
|
|
"github.com/boolean-maybe/tiki/config"
|
|
"github.com/boolean-maybe/tiki/controller"
|
|
"github.com/boolean-maybe/tiki/model"
|
|
"github.com/boolean-maybe/tiki/plugin"
|
|
"github.com/boolean-maybe/tiki/view/markdown"
|
|
|
|
"github.com/boolean-maybe/navidown/loaders"
|
|
nav "github.com/boolean-maybe/navidown/navidown"
|
|
navtview "github.com/boolean-maybe/navidown/navidown/tview"
|
|
"github.com/gdamore/tcell/v2"
|
|
"github.com/rivo/tview"
|
|
)
|
|
|
|
// DokiView renders a documentation plugin (navigable markdown)
|
|
type DokiView struct {
|
|
root *tview.Flex
|
|
titleBar tview.Primitive
|
|
md *markdown.NavigableMarkdown
|
|
pluginDef *plugin.DokiPlugin
|
|
registry *controller.ActionRegistry
|
|
imageManager *navtview.ImageManager
|
|
mermaidOpts *nav.MermaidOptions
|
|
actionChangeHandler func()
|
|
}
|
|
|
|
// NewDokiView creates a doki view
|
|
func NewDokiView(
|
|
pluginDef *plugin.DokiPlugin,
|
|
imageManager *navtview.ImageManager,
|
|
mermaidOpts *nav.MermaidOptions,
|
|
) *DokiView {
|
|
dv := &DokiView{
|
|
pluginDef: pluginDef,
|
|
registry: controller.NewActionRegistry(),
|
|
imageManager: imageManager,
|
|
mermaidOpts: mermaidOpts,
|
|
}
|
|
|
|
dv.build()
|
|
return dv
|
|
}
|
|
|
|
func (dv *DokiView) build() {
|
|
// title bar with gradient background using theme-derived caption colors
|
|
colors := config.GetColors()
|
|
pair := colors.CaptionColorForIndex(dv.pluginDef.ConfigIndex)
|
|
bgColor := pair.Background
|
|
textColor := pair.Foreground
|
|
if dv.pluginDef.ConfigIndex < 0 {
|
|
bgColor = dv.pluginDef.Background
|
|
textColor = config.DefaultColor()
|
|
}
|
|
dv.titleBar = NewGradientCaptionRow([]string{dv.pluginDef.Name}, nil, bgColor, textColor)
|
|
|
|
// Fetch initial content and create NavigableMarkdown with appropriate provider
|
|
var content string
|
|
var sourcePath string
|
|
var err error
|
|
|
|
switch dv.pluginDef.Fetcher {
|
|
case "file":
|
|
searchRoots := []string{config.GetDokiDir()}
|
|
provider := &loaders.FileHTTP{SearchRoots: searchRoots}
|
|
|
|
content, err = provider.FetchContent(nav.NavElement{URL: dv.pluginDef.URL})
|
|
|
|
// Resolve initial source path for stable relative navigation
|
|
sourcePath, _ = nav.ResolveMarkdownPath(dv.pluginDef.URL, "", searchRoots)
|
|
if sourcePath == "" {
|
|
sourcePath = dv.pluginDef.URL
|
|
}
|
|
|
|
dv.md = markdown.NewNavigableMarkdown(markdown.NavigableMarkdownConfig{
|
|
Provider: provider,
|
|
SearchRoots: searchRoots,
|
|
OnStateChange: dv.UpdateNavigationActions,
|
|
ImageManager: dv.imageManager,
|
|
MermaidOptions: dv.mermaidOpts,
|
|
})
|
|
|
|
// fetcher: internal is intentionally preserved as a supported pattern for
|
|
// code-only internal docs, even though no default workflow currently uses it.
|
|
//
|
|
// The default Help plugin previously used this with embedded markdown:
|
|
// cnt := map[string]string{"Help": helpMd, "tiki.md": tikiMd, "view.md": customMd}
|
|
// provider := &internalDokiProvider{content: cnt}
|
|
// That usage was replaced by the action palette (press Ctrl+A to open).
|
|
case "internal":
|
|
provider := &internalDokiProvider{content: map[string]string{}}
|
|
content, err = provider.FetchContent(nav.NavElement{Text: dv.pluginDef.Text})
|
|
|
|
dv.md = markdown.NewNavigableMarkdown(markdown.NavigableMarkdownConfig{
|
|
Provider: provider,
|
|
OnStateChange: dv.UpdateNavigationActions,
|
|
ImageManager: dv.imageManager,
|
|
MermaidOptions: dv.mermaidOpts,
|
|
})
|
|
|
|
default:
|
|
content = "Error: Unknown fetcher type"
|
|
dv.md = markdown.NewNavigableMarkdown(markdown.NavigableMarkdownConfig{
|
|
OnStateChange: dv.UpdateNavigationActions,
|
|
ImageManager: dv.imageManager,
|
|
MermaidOptions: dv.mermaidOpts,
|
|
})
|
|
}
|
|
|
|
if err != nil {
|
|
slog.Error("failed to fetch doki content", "plugin", dv.pluginDef.Name, "error", err)
|
|
content = fmt.Sprintf("Error loading content: %v", err)
|
|
}
|
|
|
|
// Display initial content (don't push to history - this is the first page)
|
|
if sourcePath != "" {
|
|
dv.md.SetMarkdownWithSource(content, sourcePath, false)
|
|
} else {
|
|
dv.md.SetMarkdown(content)
|
|
}
|
|
|
|
// root layout
|
|
dv.root = tview.NewFlex().SetDirection(tview.FlexRow)
|
|
dv.rebuildLayout()
|
|
}
|
|
|
|
func (dv *DokiView) rebuildLayout() {
|
|
dv.root.Clear()
|
|
dv.root.AddItem(dv.titleBar, 1, 0, false)
|
|
dv.root.AddItem(dv.md.Viewer(), 0, 1, true)
|
|
}
|
|
|
|
// ShowNavigation returns true — doki views always show plugin navigation keys.
|
|
func (dv *DokiView) ShowNavigation() bool { return true }
|
|
|
|
// GetViewName returns the plugin name for the header info section
|
|
func (dv *DokiView) GetViewName() string { return dv.pluginDef.GetName() }
|
|
|
|
// GetViewDescription returns the plugin description for the header info section
|
|
func (dv *DokiView) GetViewDescription() string { return dv.pluginDef.GetDescription() }
|
|
|
|
func (dv *DokiView) GetPrimitive() tview.Primitive {
|
|
return dv.root
|
|
}
|
|
|
|
func (dv *DokiView) GetActionRegistry() *controller.ActionRegistry {
|
|
return dv.registry
|
|
}
|
|
|
|
func (dv *DokiView) GetViewID() model.ViewID {
|
|
return model.MakePluginViewID(dv.pluginDef.Name)
|
|
}
|
|
|
|
func (dv *DokiView) OnFocus() {
|
|
// Focus behavior
|
|
}
|
|
|
|
func (dv *DokiView) OnBlur() {
|
|
if dv.md != nil {
|
|
dv.md.Close()
|
|
}
|
|
}
|
|
|
|
func (dv *DokiView) SetActionChangeHandler(handler func()) {
|
|
dv.actionChangeHandler = handler
|
|
}
|
|
|
|
// UpdateNavigationActions updates the registry to reflect current navigation state
|
|
func (dv *DokiView) UpdateNavigationActions() {
|
|
// Clear and rebuild the registry
|
|
dv.registry = controller.NewActionRegistry()
|
|
|
|
// Always show Tab/Shift+Tab for link navigation
|
|
dv.registry.Register(controller.Action{
|
|
ID: "navigate_next_link",
|
|
Key: tcell.KeyTab,
|
|
Label: "Next Link",
|
|
ShowInHeader: true,
|
|
})
|
|
dv.registry.Register(controller.Action{
|
|
ID: "navigate_prev_link",
|
|
Key: tcell.KeyBacktab,
|
|
Label: "Prev Link",
|
|
ShowInHeader: true,
|
|
})
|
|
|
|
// Add back action if available
|
|
// Note: navidown supports both plain Left/Right and Alt+Left/Right for navigation
|
|
// We register plain arrows since they're simpler and work in all terminals
|
|
if dv.md.CanGoBack() {
|
|
dv.registry.Register(controller.Action{
|
|
ID: controller.ActionNavigateBack,
|
|
Key: tcell.KeyLeft,
|
|
Label: "← Back",
|
|
ShowInHeader: true,
|
|
})
|
|
}
|
|
|
|
// Add forward action if available
|
|
if dv.md.CanGoForward() {
|
|
dv.registry.Register(controller.Action{
|
|
ID: controller.ActionNavigateForward,
|
|
Key: tcell.KeyRight,
|
|
Label: "Forward →",
|
|
ShowInHeader: true,
|
|
})
|
|
}
|
|
|
|
if dv.actionChangeHandler != nil {
|
|
dv.actionChangeHandler()
|
|
}
|
|
}
|
|
|
|
// HandlePaletteAction maps palette-dispatched actions to the markdown viewer's
|
|
// existing key-driven behavior by replaying synthetic key events.
|
|
func (dv *DokiView) HandlePaletteAction(id controller.ActionID) bool {
|
|
if dv.md == nil {
|
|
return false
|
|
}
|
|
var event *tcell.EventKey
|
|
switch id {
|
|
case "navigate_next_link":
|
|
event = tcell.NewEventKey(tcell.KeyTab, 0, tcell.ModNone)
|
|
case "navigate_prev_link":
|
|
event = tcell.NewEventKey(tcell.KeyBacktab, 0, tcell.ModNone)
|
|
case controller.ActionNavigateBack:
|
|
event = tcell.NewEventKey(tcell.KeyLeft, 0, tcell.ModNone)
|
|
case controller.ActionNavigateForward:
|
|
event = tcell.NewEventKey(tcell.KeyRight, 0, tcell.ModNone)
|
|
default:
|
|
return false
|
|
}
|
|
handler := dv.md.Viewer().InputHandler()
|
|
if handler != nil {
|
|
handler(event, nil)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// internalDokiProvider implements navidown.ContentProvider for embedded/internal docs.
|
|
// It treats elem.URL as the lookup key, falling back to elem.Text for initial loads.
|
|
type internalDokiProvider struct {
|
|
content map[string]string
|
|
}
|
|
|
|
func (p *internalDokiProvider) FetchContent(elem nav.NavElement) (string, error) {
|
|
if p == nil {
|
|
return "", nil
|
|
}
|
|
// Use URL for link navigation, Text for initial load
|
|
key := elem.URL
|
|
if key == "" {
|
|
key = elem.Text
|
|
}
|
|
return p.content[key], nil
|
|
}
|