mirror of
https://github.com/boolean-maybe/tiki
synced 2026-04-21 13:37:20 +00:00
SVG and mermaid support
This commit is contained in:
parent
98a878b6da
commit
31e32ea791
18 changed files with 61 additions and 36 deletions
|
|
@ -151,5 +151,5 @@ func brailleColumnMask(level int, rightColumn bool) uint8 {
|
|||
|
||||
func brailleRuneForCounts(leftCount, rightCount int) rune {
|
||||
mask := brailleColumnMask(leftCount, false) | brailleColumnMask(rightCount, true)
|
||||
return rune(0x2800 + int(mask))
|
||||
return rune(0x2800 + int(mask)) //nolint:gosec // G115: mask is uint8 (0-255), result is 0x2800..0x28FF, valid braille rune range
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ func GetArtTView() string {
|
|||
colorIdx = len(currentGradient) - 1
|
||||
}
|
||||
color := currentGradient[colorIdx]
|
||||
result.WriteString(fmt.Sprintf("[%s]%s[white]\n", color, line))
|
||||
fmt.Fprintf(&result, "[%s]%s[white]\n", color, line)
|
||||
}
|
||||
return result.String()
|
||||
}
|
||||
|
|
@ -73,7 +73,7 @@ func getDotsArtTView() string {
|
|||
result.WriteRune(char)
|
||||
continue
|
||||
}
|
||||
result.WriteString(fmt.Sprintf("[%s]%c", color, char))
|
||||
fmt.Fprintf(&result, "[%s]%c", color, char)
|
||||
}
|
||||
result.WriteString("[white]\n")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -315,8 +315,8 @@ func TestActionRegistry_GetHeaderActions(t *testing.T) {
|
|||
|
||||
expectedIDs := []ActionID{ActionQuit}
|
||||
for i, action := range headerActions {
|
||||
if action.ID != expectedIDs[i] {
|
||||
t.Errorf("header action %d: expected %v, got %v", i, expectedIDs[i], action.ID)
|
||||
if action.ID != expectedIDs[i] { //nolint:gosec // G602: len verified equal on line 312
|
||||
t.Errorf("header action %d: expected %v, got %v", i, expectedIDs[i], action.ID) //nolint:gosec // G602: len verified equal on line 312
|
||||
}
|
||||
if !action.ShowInHeader {
|
||||
t.Errorf("header action %d: ShowInHeader should be true", i)
|
||||
|
|
|
|||
2
go.mod
2
go.mod
|
|
@ -3,7 +3,7 @@ module github.com/boolean-maybe/tiki
|
|||
go 1.24.2
|
||||
|
||||
require (
|
||||
github.com/boolean-maybe/navidown v0.3.1
|
||||
github.com/boolean-maybe/navidown v0.4.0
|
||||
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7
|
||||
github.com/charmbracelet/bubbletea v1.3.10
|
||||
github.com/charmbracelet/huh v0.8.0
|
||||
|
|
|
|||
4
go.sum
4
go.sum
|
|
@ -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.3.1 h1:Q5uUIxF28wCWSSK7JRUb79dbkZkwYbRA0FDf7IAspts=
|
||||
github.com/boolean-maybe/navidown v0.3.1/go.mod h1:WdI3A007LaGuaJTOwEVO25kMojD9u4SCEVqAT8H9rwQ=
|
||||
github.com/boolean-maybe/navidown v0.4.0 h1:HnO7Ck58O4DDGWQ21V8b00y3hRSQ0iS6itVCFyWDVgI=
|
||||
github.com/boolean-maybe/navidown v0.4.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=
|
||||
|
|
|
|||
|
|
@ -163,7 +163,7 @@ func TestEditSource_DuplicateCaseIDs_Repro(t *testing.T) {
|
|||
return err
|
||||
}
|
||||
content = append(content, '\n')
|
||||
return os.WriteFile(path, content, 0644)
|
||||
return os.WriteFile(path, content, 0644) //nolint:gosec // G703: path is a controlled temp file provided by the app
|
||||
})
|
||||
|
||||
// Open task detail view directly.
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ func Bootstrap(tikiSkillContent, dokiSkillContent string) (*Result, error) {
|
|||
wireOnViewActivated(rootLayout, application)
|
||||
|
||||
// Phase 11: Background tasks
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(context.Background()) //nolint:gosec // G118: cancel stored in Result.CancelFunc, called by app shutdown
|
||||
background.StartBurndownHistoryBuilder(ctx, tikiStore, headerConfig, application)
|
||||
|
||||
// Phase 12: Navigation and input wiring
|
||||
|
|
|
|||
|
|
@ -42,10 +42,12 @@ func Run(input InputSpec) error {
|
|||
|
||||
// Create NavigableMarkdown - OnStateChange is set after creation to avoid forward reference
|
||||
md := view.NewNavigableMarkdown(view.NavigableMarkdownConfig{
|
||||
Provider: provider,
|
||||
SearchRoots: input.SearchRoots,
|
||||
ImageManager: imgMgr,
|
||||
Provider: provider,
|
||||
SearchRoots: input.SearchRoots,
|
||||
ImageManager: imgMgr,
|
||||
MermaidOptions: &nav.MermaidOptions{},
|
||||
})
|
||||
defer md.Close()
|
||||
md.SetStateChangedHandler(func() {
|
||||
updateStatusBar(statusBar, md.Viewer())
|
||||
})
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ func parseKey(s string) (tcell.Key, rune, tcell.ModMask, error) {
|
|||
char := []rune(rest)
|
||||
if len(char) == 1 && char[0] >= 'A' && char[0] <= 'Z' {
|
||||
// tcell.KeyCtrlA is 65 ('A'), KeyCtrlZ is 90 ('Z')
|
||||
return tcell.Key(char[0]), 0, tcell.ModCtrl, nil
|
||||
return tcell.Key(char[0]), 0, tcell.ModCtrl, nil //nolint:gosec // G115: char[0] bounded 'A'-'Z' (65-90), safe to convert to int16
|
||||
}
|
||||
return 0, 0, 0, fmt.Errorf("invalid ctrl key: %q (expected Ctrl-A..Ctrl-Z or Ctrl-F1..Ctrl-F12)", s)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ func (u *Util) LastCommitTime(filePath string) (time.Time, error) {
|
|||
// Returns a map of file paths to their last commit time.
|
||||
func (u *Util) AllLastCommitTimes(dirPattern string) (map[string]time.Time, error) {
|
||||
// Get all commits (most recent first due to default reverse chronological order)
|
||||
cmd := exec.Command("git", "log", "--all", "--format=%aI", "--name-only", "--", dirPattern)
|
||||
cmd := exec.Command("git", "log", "--all", "--format=%aI", "--name-only", "--", dirPattern) //nolint:gosec // G204: git command with controlled directory pattern
|
||||
cmd.Dir = u.repoPath
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ func (u *Util) Author(filePath string) (*AuthorInfo, error) {
|
|||
// Returns a map of file paths to their author info.
|
||||
func (u *Util) AllAuthors(dirPattern string) (map[string]*AuthorInfo, error) {
|
||||
// Use git log with --diff-filter=A (added files), --name-only, and --reverse to get creation info
|
||||
cmd := exec.Command("git", "log", "--all", "--diff-filter=A", "--format=%H|%an|%ae|%ai|%s", "--name-only", "--reverse", "--", dirPattern)
|
||||
cmd := exec.Command("git", "log", "--all", "--diff-filter=A", "--format=%H|%an|%ae|%ai|%s", "--name-only", "--reverse", "--", dirPattern) //nolint:gosec // G204: git command with controlled directory pattern
|
||||
cmd.Dir = u.repoPath
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -34,17 +34,20 @@ type DokiView struct {
|
|||
pluginDef *plugin.DokiPlugin
|
||||
registry *controller.ActionRegistry
|
||||
imageManager *navtview.ImageManager
|
||||
mermaidOpts *nav.MermaidOptions
|
||||
}
|
||||
|
||||
// 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()
|
||||
|
|
@ -78,10 +81,11 @@ func (dv *DokiView) build() {
|
|||
}
|
||||
|
||||
dv.markdown = NewNavigableMarkdown(NavigableMarkdownConfig{
|
||||
Provider: provider,
|
||||
SearchRoots: searchRoots,
|
||||
OnStateChange: dv.UpdateNavigationActions,
|
||||
ImageManager: dv.imageManager,
|
||||
Provider: provider,
|
||||
SearchRoots: searchRoots,
|
||||
OnStateChange: dv.UpdateNavigationActions,
|
||||
ImageManager: dv.imageManager,
|
||||
MermaidOptions: dv.mermaidOpts,
|
||||
})
|
||||
|
||||
case "internal":
|
||||
|
|
@ -94,16 +98,18 @@ func (dv *DokiView) build() {
|
|||
content, err = provider.FetchContent(nav.NavElement{Text: dv.pluginDef.Text})
|
||||
|
||||
dv.markdown = NewNavigableMarkdown(NavigableMarkdownConfig{
|
||||
Provider: provider,
|
||||
OnStateChange: dv.UpdateNavigationActions,
|
||||
ImageManager: dv.imageManager,
|
||||
Provider: provider,
|
||||
OnStateChange: dv.UpdateNavigationActions,
|
||||
ImageManager: dv.imageManager,
|
||||
MermaidOptions: dv.mermaidOpts,
|
||||
})
|
||||
|
||||
default:
|
||||
content = "Error: Unknown fetcher type"
|
||||
dv.markdown = NewNavigableMarkdown(NavigableMarkdownConfig{
|
||||
OnStateChange: dv.UpdateNavigationActions,
|
||||
ImageManager: dv.imageManager,
|
||||
OnStateChange: dv.UpdateNavigationActions,
|
||||
ImageManager: dv.imageManager,
|
||||
MermaidOptions: dv.mermaidOpts,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import (
|
|||
type ViewFactory struct {
|
||||
taskStore store.Store
|
||||
imageManager *navtview.ImageManager
|
||||
mermaidOpts *nav.MermaidOptions
|
||||
// Plugin support
|
||||
pluginConfigs map[string]*model.PluginConfig
|
||||
pluginDefs map[string]plugin.Plugin
|
||||
|
|
@ -39,6 +40,7 @@ func NewViewFactory(taskStore store.Store) *ViewFactory {
|
|||
return &ViewFactory{
|
||||
taskStore: taskStore,
|
||||
imageManager: imgMgr,
|
||||
mermaidOpts: &nav.MermaidOptions{},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -60,7 +62,7 @@ func (f *ViewFactory) CreateView(viewID model.ViewID, params map[string]interfac
|
|||
switch viewID {
|
||||
case model.TaskDetailViewID:
|
||||
taskID := model.DecodeTaskDetailParams(params).TaskID
|
||||
v = taskdetail.NewTaskDetailView(f.taskStore, taskID, f.imageManager)
|
||||
v = taskdetail.NewTaskDetailView(f.taskStore, taskID, f.imageManager, f.mermaidOpts)
|
||||
|
||||
case model.TaskEditViewID:
|
||||
taskID := model.DecodeTaskEditParams(params).TaskID
|
||||
|
|
@ -96,7 +98,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.imageManager)
|
||||
v = NewDokiView(dokiPlugin, f.imageManager, f.mermaidOpts)
|
||||
} else {
|
||||
slog.Error("unknown plugin type or missing config", "plugin", pluginName)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -237,7 +237,7 @@ func buildGridRow(rowData []cellData, maxKeyLenPerCol, maxLabelLenPerCol []int,
|
|||
|
||||
// Render cell with colors
|
||||
scheme := getColorScheme(cell.colorType)
|
||||
line.WriteString(fmt.Sprintf("[%s]<%s>[%s]", scheme.KeyColor, cell.key, scheme.LabelColor))
|
||||
fmt.Fprintf(&line, "[%s]<%s>[%s]", scheme.KeyColor, cell.key, scheme.LabelColor)
|
||||
|
||||
// Add key padding
|
||||
if keyPadding := maxKeyLenPerCol[col] - cell.keyLen; keyPadding > 0 {
|
||||
|
|
|
|||
|
|
@ -20,10 +20,11 @@ type NavigableMarkdown struct {
|
|||
|
||||
// NavigableMarkdownConfig configures a NavigableMarkdown component.
|
||||
type NavigableMarkdownConfig struct {
|
||||
Provider nav.ContentProvider
|
||||
SearchRoots []string
|
||||
OnStateChange func() // called on navigation state changes
|
||||
ImageManager *navtview.ImageManager
|
||||
Provider nav.ContentProvider
|
||||
SearchRoots []string
|
||||
OnStateChange func() // called on navigation state changes
|
||||
ImageManager *navtview.ImageManager
|
||||
MermaidOptions *nav.MermaidOptions // nil = disabled
|
||||
}
|
||||
|
||||
// NewNavigableMarkdown creates a new navigable markdown viewer.
|
||||
|
|
@ -45,10 +46,18 @@ func NewNavigableMarkdown(cfg NavigableMarkdownConfig) *NavigableMarkdown {
|
|||
nm.onStateChange()
|
||||
}
|
||||
})
|
||||
if cfg.MermaidOptions != nil {
|
||||
nm.viewer.Core().SetMermaidOptions(cfg.MermaidOptions)
|
||||
}
|
||||
nm.setupSelectHandler()
|
||||
return nm
|
||||
}
|
||||
|
||||
// Close releases resources held by the navigable markdown (e.g., mermaid temp files).
|
||||
func (nm *NavigableMarkdown) Close() {
|
||||
nm.viewer.Core().Close()
|
||||
}
|
||||
|
||||
func (nm *NavigableMarkdown) setupSelectHandler() {
|
||||
nm.viewer.SetSelectHandler(func(v *navtview.TextViewViewer, elem nav.NavElement) {
|
||||
if elem.Type != nav.NavElementURL {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"github.com/boolean-maybe/tiki/store"
|
||||
taskpkg "github.com/boolean-maybe/tiki/task"
|
||||
|
||||
nav "github.com/boolean-maybe/navidown/navidown"
|
||||
navtview "github.com/boolean-maybe/navidown/navidown/tview"
|
||||
"github.com/rivo/tview"
|
||||
)
|
||||
|
|
@ -20,6 +21,7 @@ type Base struct {
|
|||
taskStore store.Store
|
||||
taskID string
|
||||
imageManager *navtview.ImageManager
|
||||
mermaidOpts *nav.MermaidOptions
|
||||
descView tview.Primitive
|
||||
|
||||
// Task data
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ func TestBuildMetadataColumns_Structure(t *testing.T) {
|
|||
UpdatedAt: time.Date(2024, 1, 2, 14, 30, 0, 0, time.UTC),
|
||||
}
|
||||
|
||||
view := NewTaskDetailView(s, task.ID, nil)
|
||||
view := NewTaskDetailView(s, task.ID, nil, nil)
|
||||
view.SetFallbackTask(task)
|
||||
|
||||
colors := config.GetColors()
|
||||
|
|
@ -62,7 +62,7 @@ func TestBuildMetadataColumns_Column1Fields(t *testing.T) {
|
|||
Priority: 3,
|
||||
}
|
||||
|
||||
view := NewTaskDetailView(s, task.ID, nil)
|
||||
view := NewTaskDetailView(s, task.ID, nil, nil)
|
||||
view.SetFallbackTask(task)
|
||||
|
||||
colors := config.GetColors()
|
||||
|
|
@ -88,7 +88,7 @@ func TestBuildMetadataColumns_Column2Fields(t *testing.T) {
|
|||
Points: 5,
|
||||
}
|
||||
|
||||
view := NewTaskDetailView(s, task.ID, nil)
|
||||
view := NewTaskDetailView(s, task.ID, nil, nil)
|
||||
view.SetFallbackTask(task)
|
||||
|
||||
colors := config.GetColors()
|
||||
|
|
|
|||
|
|
@ -30,12 +30,13 @@ type TaskDetailView struct {
|
|||
}
|
||||
|
||||
// NewTaskDetailView creates a task detail view in read-only mode
|
||||
func NewTaskDetailView(taskStore store.Store, taskID string, imageManager *navtview.ImageManager) *TaskDetailView {
|
||||
func NewTaskDetailView(taskStore store.Store, taskID string, imageManager *navtview.ImageManager, mermaidOpts *nav.MermaidOptions) *TaskDetailView {
|
||||
tv := &TaskDetailView{
|
||||
Base: Base{
|
||||
taskStore: taskStore,
|
||||
taskID: taskID,
|
||||
imageManager: imageManager,
|
||||
mermaidOpts: mermaidOpts,
|
||||
},
|
||||
registry: controller.TaskDetailViewActions(),
|
||||
viewID: model.TaskDetailViewID,
|
||||
|
|
@ -178,6 +179,9 @@ func (tv *TaskDetailView) buildDescription(task *taskpkg.Task) tview.Primitive {
|
|||
if tv.imageManager != nil && tv.imageManager.Supported() {
|
||||
viewer.SetImageManager(tv.imageManager)
|
||||
}
|
||||
if tv.mermaidOpts != nil {
|
||||
viewer.Core().SetMermaidOptions(tv.mermaidOpts)
|
||||
}
|
||||
// Use SetMarkdownWithSource to provide the source file path for relative image resolution
|
||||
viewer.SetMarkdownWithSource(desc, taskSourcePath, false)
|
||||
viewer.SetBorderPadding(1, 1, 2, 2)
|
||||
|
|
|
|||
Loading…
Reference in a new issue