support Kitty images

This commit is contained in:
booleanmaybe 2026-03-06 18:31:05 -05:00
parent 54e1fdb69f
commit f49d55b19f
9 changed files with 84 additions and 20 deletions

View file

@ -57,6 +57,7 @@ header:
# Tiki settings
tiki:
maxPoints: 10 # Maximum story points for tasks
maxImageRows: 40 # Maximum rows for inline images (Kitty protocol)
# Logging settings
logging:

View file

@ -35,7 +35,8 @@ type Config struct {
// Tiki configuration
Tiki struct {
MaxPoints int `mapstructure:"maxPoints"`
MaxPoints int `mapstructure:"maxPoints"`
MaxImageRows int `mapstructure:"maxImageRows"`
} `mapstructure:"tiki"`
// Appearance configuration
@ -110,6 +111,7 @@ func setDefaults() {
// Tiki defaults
viper.SetDefault("tiki.maxPoints", 10)
viper.SetDefault("tiki.maxImageRows", 40)
// Appearance defaults
viper.SetDefault("appearance.theme", "auto")
@ -277,6 +279,15 @@ func GetMaxPoints() int {
return maxPoints
}
// GetMaxImageRows returns the maximum rows for inline image rendering
func GetMaxImageRows() int {
rows := viper.GetInt("tiki.maxImageRows")
if rows < 1 {
return 40
}
return rows
}
// saveConfig writes the current viper configuration to config.yaml
func saveConfig() error {
configFile := viper.ConfigFileUsed()

2
go.mod
View file

@ -3,7 +3,7 @@ module github.com/boolean-maybe/tiki
go 1.24.2
require (
github.com/boolean-maybe/navidown v0.2.4
github.com/boolean-maybe/navidown v0.3.0
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/glamour v0.10.0

4
go.sum
View file

@ -25,8 +25,8 @@ github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3v
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/boolean-maybe/navidown v0.2.4 h1:If3LrcoLv4T/6VnWOjiVhseL9YR74Uc8gLv9Vta9bIc=
github.com/boolean-maybe/navidown v0.2.4/go.mod h1:WdI3A007LaGuaJTOwEVO25kMojD9u4SCEVqAT8H9rwQ=
github.com/boolean-maybe/navidown v0.3.0 h1:xhOvo7o+8MFyXb8scbWk+63PnJrvrlfzdtOnEWGruME=
github.com/boolean-maybe/navidown v0.3.0/go.mod h1:WdI3A007LaGuaJTOwEVO25kMojD9u4SCEVqAT8H9rwQ=
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws=

View file

@ -34,10 +34,17 @@ func Run(input InputSpec) error {
statusBar.SetDynamicColors(true)
statusBar.SetTextAlign(tview.AlignLeft)
// Set up image rendering for Kitty-compatible terminals
resolver := nav.NewImageResolver(input.SearchRoots)
imgMgr := navtview.NewImageManager(resolver, 8, 16)
imgMgr.SetMaxRows(config.GetMaxImageRows())
imgMgr.SetSupported(util.SupportsKittyGraphics())
// Create NavigableMarkdown - OnStateChange is set after creation to avoid forward reference
md := view.NewNavigableMarkdown(view.NavigableMarkdownConfig{
Provider: provider,
SearchRoots: input.SearchRoots,
Provider: provider,
SearchRoots: input.SearchRoots,
ImageManager: imgMgr,
})
md.SetStateChangedHandler(func() {
updateStatusBar(statusBar, md.Viewer())

23
util/terminal.go Normal file
View file

@ -0,0 +1,23 @@
package util
import "os"
// SupportsKittyGraphics returns true if the terminal supports the Kitty
// graphics protocol (Unicode placeholders). Detection is env-var based:
// Kitty sets KITTY_WINDOW_ID, WezTerm sets WEZTERM_EXECUTABLE, Ghostty
// sets GHOSTTY_RESOURCES_DIR, and Konsole identifies via TERM_PROGRAM.
func SupportsKittyGraphics() bool {
if os.Getenv("KITTY_WINDOW_ID") != "" {
return true
}
if os.Getenv("WEZTERM_EXECUTABLE") != "" {
return true
}
if os.Getenv("GHOSTTY_RESOURCES_DIR") != "" {
return true
}
if os.Getenv("TERM_PROGRAM") == "Konsole" {
return true
}
return false
}

View file

@ -13,6 +13,7 @@ import (
"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"
)
@ -28,23 +29,26 @@ var customMd string
// DokiView renders a documentation plugin (navigable markdown)
type DokiView struct {
root *tview.Flex
titleBar tview.Primitive
markdown *NavigableMarkdown
pluginDef *plugin.DokiPlugin
registry *controller.ActionRegistry
renderer renderer.MarkdownRenderer
root *tview.Flex
titleBar tview.Primitive
markdown *NavigableMarkdown
pluginDef *plugin.DokiPlugin
registry *controller.ActionRegistry
renderer renderer.MarkdownRenderer
imageManager *navtview.ImageManager
}
// NewDokiView creates a doki view
func NewDokiView(
pluginDef *plugin.DokiPlugin,
mdRenderer renderer.MarkdownRenderer,
imageManager *navtview.ImageManager,
) *DokiView {
dv := &DokiView{
pluginDef: pluginDef,
registry: controller.NewActionRegistry(),
renderer: mdRenderer,
pluginDef: pluginDef,
registry: controller.NewActionRegistry(),
renderer: mdRenderer,
imageManager: imageManager,
}
dv.build()
@ -81,6 +85,7 @@ func (dv *DokiView) build() {
Provider: provider,
SearchRoots: searchRoots,
OnStateChange: dv.UpdateNavigationActions,
ImageManager: dv.imageManager,
})
case "internal":
@ -95,12 +100,14 @@ func (dv *DokiView) build() {
dv.markdown = NewNavigableMarkdown(NavigableMarkdownConfig{
Provider: provider,
OnStateChange: dv.UpdateNavigationActions,
ImageManager: dv.imageManager,
})
default:
content = "Error: Unknown fetcher type"
dv.markdown = NewNavigableMarkdown(NavigableMarkdownConfig{
OnStateChange: dv.UpdateNavigationActions,
ImageManager: dv.imageManager,
})
}

View file

@ -3,10 +3,14 @@ package view
import (
"log/slog"
nav "github.com/boolean-maybe/navidown/navidown"
navtview "github.com/boolean-maybe/navidown/navidown/tview"
"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/store"
"github.com/boolean-maybe/tiki/util"
"github.com/boolean-maybe/tiki/view/renderer"
"github.com/boolean-maybe/tiki/view/taskdetail"
)
@ -16,8 +20,9 @@ import (
// ViewFactory creates views on demand
type ViewFactory struct {
taskStore store.Store
renderer renderer.MarkdownRenderer
taskStore store.Store
renderer renderer.MarkdownRenderer
imageManager *navtview.ImageManager
// Plugin support
pluginConfigs map[string]*model.PluginConfig
pluginDefs map[string]plugin.Plugin
@ -35,9 +40,15 @@ func NewViewFactory(taskStore store.Store) *ViewFactory {
mdRenderer = glamourRenderer
}
resolver := nav.NewImageResolver(nil)
imgMgr := navtview.NewImageManager(resolver, 8, 16)
imgMgr.SetMaxRows(config.GetMaxImageRows())
imgMgr.SetSupported(util.SupportsKittyGraphics())
return &ViewFactory{
taskStore: taskStore,
renderer: mdRenderer,
taskStore: taskStore,
renderer: mdRenderer,
imageManager: imgMgr,
}
}
@ -95,7 +106,7 @@ func (f *ViewFactory) CreateView(viewID model.ViewID, params map[string]interfac
slog.Error("plugin controller type mismatch", "plugin", pluginName)
}
} else if dokiPlugin, ok := pluginDef.(*plugin.DokiPlugin); ok {
v = NewDokiView(dokiPlugin, f.renderer)
v = NewDokiView(dokiPlugin, f.renderer, f.imageManager)
} else {
slog.Error("unknown plugin type or missing config", "plugin", pluginName)
}

View file

@ -23,6 +23,7 @@ type NavigableMarkdownConfig struct {
Provider nav.ContentProvider
SearchRoots []string
OnStateChange func() // called on navigation state changes
ImageManager *navtview.ImageManager
}
// NewNavigableMarkdown creates a new navigable markdown viewer.
@ -36,6 +37,9 @@ func NewNavigableMarkdown(cfg NavigableMarkdownConfig) *NavigableMarkdown {
nm.viewer.SetAnsiConverter(navutil.NewAnsiConverter(true))
nm.viewer.SetRenderer(nav.NewANSIRendererWithStyle(config.GetEffectiveTheme()))
nm.viewer.SetBackgroundColor(config.GetContentBackgroundColor())
if cfg.ImageManager != nil && cfg.ImageManager.Supported() {
nm.viewer.SetImageManager(cfg.ImageManager)
}
nm.viewer.SetStateChangedHandler(func(_ *navtview.TextViewViewer) {
if nm.onStateChange != nil {
nm.onStateChange()