• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

charmbracelet / glow / 14244739488

03 Apr 2025 01:52PM UTC coverage: 3.722% (-0.007%) from 3.729%
14244739488

Pull #735

github

web-flow
chore: fix typo in error

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Pull Request #735: chore: do some basic repo maintenance (editorconfig, linting, etc)

0 of 85 new or added lines in 14 files covered. (0.0%)

1 existing line in 1 file now uncovered.

77 of 2069 relevant lines covered (3.72%)

0.05 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/ui/stash.go
1
package ui
2

3
import (
4
        "errors"
5
        "fmt"
6
        "os"
7
        "sort"
8
        "strings"
9
        "time"
10

11
        "github.com/charmbracelet/bubbles/paginator"
12
        "github.com/charmbracelet/bubbles/spinner"
13
        "github.com/charmbracelet/bubbles/textinput"
14
        tea "github.com/charmbracelet/bubbletea"
15
        "github.com/charmbracelet/lipgloss"
16
        "github.com/charmbracelet/log"
17
        "github.com/muesli/reflow/ansi"
18
        "github.com/muesli/reflow/truncate"
19
        "github.com/sahilm/fuzzy"
20
)
21

22
const (
23
        stashIndent                = 1
24
        stashViewItemHeight        = 3 // height of stash entry, including gap
25
        stashViewTopPadding        = 5 // logo, status bar, gaps
26
        stashViewBottomPadding     = 3 // pagination and gaps, but not help
27
        stashViewHorizontalPadding = 6
28
)
29

30
var stashingStatusMessage = statusMessage{normalStatusMessage, "Stashing..."}
31

32
var (
33
        dividerDot = darkGrayFg.SetString(" • ")
34
        dividerBar = darkGrayFg.SetString(" │ ")
35

36
        logoStyle = lipgloss.NewStyle().
37
                        Foreground(lipgloss.Color("#ECFD65")).
38
                        Background(fuchsia).
39
                        Bold(true)
40

41
        stashSpinnerStyle = lipgloss.NewStyle().
42
                                Foreground(gray)
43
        stashInputPromptStyle = lipgloss.NewStyle().
44
                                Foreground(yellowGreen).
45
                                MarginRight(1)
46
        stashInputCursorStyle = lipgloss.NewStyle().
47
                                Foreground(fuchsia).
48
                                MarginRight(1)
49
)
50

51
// MSG
52

53
type (
54
        filteredMarkdownMsg []*markdown
55
        fetchedMarkdownMsg  *markdown
56
)
57

58
// MODEL
59

60
// stashViewState is the high-level state of the file listing.
61
type stashViewState int
62

63
const (
64
        stashStateReady stashViewState = iota
65
        stashStateLoadingDocument
66
        stashStateShowingError
67
)
68

69
// The types of documents we are currently showing to the user.
70
type sectionKey int
71

72
const (
73
        documentsSection = iota
74
        filterSection
75
)
76

77
// section contains definitions and state information for displaying a tab and
78
// its contents in the file listing view.
79
type section struct {
80
        key       sectionKey
81
        paginator paginator.Model
82
        cursor    int
83
}
84

85
// map sections to their associated types.
86
var sections = map[sectionKey]section{}
87

88
// filterState is the current filtering state in the file listing.
89
type filterState int
90

91
const (
92
        unfiltered    filterState = iota // no filter set
93
        filtering                        // user is actively setting a filter
94
        filterApplied                    // a filter is applied and user is not editing filter
95
)
96

97
// statusMessageType adds some context to the status message being sent.
98
type statusMessageType int
99

100
// Types of status messages.
101
const (
102
        normalStatusMessage statusMessageType = iota
103
        subtleStatusMessage
104
        errorStatusMessage
105
)
106

107
// statusMessage is an ephemeral note displayed in the UI.
108
type statusMessage struct {
109
        status  statusMessageType
110
        message string
111
}
112

113
func initSections() {
×
114
        sections = map[sectionKey]section{
×
115
                documentsSection: {
×
116
                        key:       documentsSection,
×
117
                        paginator: newStashPaginator(),
×
118
                },
×
119
                filterSection: {
×
120
                        key:       filterSection,
×
121
                        paginator: newStashPaginator(),
×
122
                },
×
123
        }
×
124
}
×
125

126
// String returns a styled version of the status message appropriate for the
127
// given context.
128
func (s statusMessage) String() string {
×
NEW
129
        switch s.status { //nolint:exhaustive
×
130
        case subtleStatusMessage:
×
131
                return dimGreenFg(s.message)
×
132
        case errorStatusMessage:
×
133
                return redFg(s.message)
×
134
        default:
×
135
                return greenFg(s.message)
×
136
        }
137
}
138

139
type stashModel struct {
140
        common             *commonModel
141
        err                error
142
        spinner            spinner.Model
143
        filterInput        textinput.Model
144
        viewState          stashViewState
145
        filterState        filterState
146
        showFullHelp       bool
147
        showStatusMessage  bool
148
        statusMessage      statusMessage
149
        statusMessageTimer *time.Timer
150

151
        // Available document sections we can cycle through. We use a slice, rather
152
        // than a map, because order is important.
153
        sections []section
154

155
        // Index of the section we're currently looking at
156
        sectionIndex int
157

158
        // Tracks if docs were loaded
159
        loaded bool
160

161
        // The master set of markdown documents we're working with.
162
        markdowns []*markdown
163

164
        // Markdown documents we're currently displaying. Filtering, toggles and so
165
        // on will alter this slice so we can show what is relevant. For that
166
        // reason, this field should be considered ephemeral.
167
        filteredMarkdowns []*markdown
168

169
        // Page we're fetching stash items from on the server, which is different
170
        // from the local pagination. Generally, the server will return more items
171
        // than we can display at a time so we can paginate locally without having
172
        // to fetch every time.
173
        serverPage int64
174
}
175

176
func (m stashModel) loadingDone() bool {
×
177
        return m.loaded
×
178
}
×
179

180
func (m stashModel) currentSection() *section {
×
181
        return &m.sections[m.sectionIndex]
×
182
}
×
183

184
func (m stashModel) paginator() *paginator.Model {
×
185
        return &m.currentSection().paginator
×
186
}
×
187

188
func (m *stashModel) setPaginator(p paginator.Model) {
×
189
        m.currentSection().paginator = p
×
190
}
×
191

192
func (m stashModel) cursor() int {
×
193
        return m.currentSection().cursor
×
194
}
×
195

196
func (m *stashModel) setCursor(i int) {
×
197
        m.currentSection().cursor = i
×
198
}
×
199

200
// Whether or not the spinner should be spinning.
201
func (m stashModel) shouldSpin() bool {
×
202
        loading := !m.loadingDone()
×
203
        openingDocument := m.viewState == stashStateLoadingDocument
×
204
        return loading || openingDocument
×
205
}
×
206

207
func (m *stashModel) setSize(width, height int) {
×
208
        m.common.width = width
×
209
        m.common.height = height
×
210

×
211
        m.filterInput.Width = width - stashViewHorizontalPadding*2 - ansi.PrintableRuneWidth(
×
212
                m.filterInput.Prompt,
×
213
        )
×
214

×
215
        m.updatePagination()
×
216
}
×
217

218
func (m *stashModel) resetFiltering() {
×
219
        m.filterState = unfiltered
×
220
        m.filterInput.Reset()
×
221
        m.filteredMarkdowns = nil
×
222

×
223
        sortMarkdowns(m.markdowns)
×
224

×
225
        // If the filtered section is present (it's always at the end) slice it out
×
226
        // of the sections slice to remove it from the UI.
×
227
        if m.sections[len(m.sections)-1].key == filterSection {
×
228
                m.sections = m.sections[:len(m.sections)-1]
×
229
        }
×
230

231
        // If the current section is out of bounds (it would be if we cut down the
232
        // slice above) then return to the first section.
233
        if m.sectionIndex > len(m.sections)-1 {
×
234
                m.sectionIndex = 0
×
235
        }
×
236

237
        // Update pagination after we've switched sections.
238
        m.updatePagination()
×
239
}
240

241
// Is a filter currently being applied?
242
func (m stashModel) filterApplied() bool {
×
243
        return m.filterState != unfiltered
×
244
}
×
245

246
// Should we be updating the filter?
247
func (m stashModel) shouldUpdateFilter() bool {
×
248
        // If we're in the middle of setting a note don't update the filter so that
×
249
        // the focus won't jump around.
×
250
        return m.filterApplied()
×
251
}
×
252

253
// Update pagination according to the amount of markdowns for the current
254
// state.
255
func (m *stashModel) updatePagination() {
×
256
        _, helpHeight := m.helpView()
×
257

×
258
        availableHeight := m.common.height -
×
259
                stashViewTopPadding -
×
260
                helpHeight -
×
261
                stashViewBottomPadding
×
262

×
263
        m.paginator().PerPage = max(1, availableHeight/stashViewItemHeight)
×
264

×
265
        if pages := len(m.getVisibleMarkdowns()); pages < 1 {
×
266
                m.paginator().SetTotalPages(1)
×
267
        } else {
×
268
                m.paginator().SetTotalPages(pages)
×
269
        }
×
270

271
        // Make sure the page stays in bounds
272
        if m.paginator().Page >= m.paginator().TotalPages-1 {
×
273
                m.paginator().Page = max(0, m.paginator().TotalPages-1)
×
274
        }
×
275
}
276

277
// MarkdownIndex returns the index of the currently selected markdown item.
278
func (m stashModel) markdownIndex() int {
×
279
        return m.paginator().Page*m.paginator().PerPage + m.cursor()
×
280
}
×
281

282
// Return the current selected markdown in the stash.
283
func (m stashModel) selectedMarkdown() *markdown {
×
284
        i := m.markdownIndex()
×
285

×
286
        mds := m.getVisibleMarkdowns()
×
287
        if i < 0 || len(mds) == 0 || len(mds) <= i {
×
288
                return nil
×
289
        }
×
290

291
        return mds[i]
×
292
}
293

294
// Adds markdown documents to the model.
295
func (m *stashModel) addMarkdowns(mds ...*markdown) {
×
296
        if len(mds) == 0 {
×
297
                return
×
298
        }
×
299

300
        m.markdowns = append(m.markdowns, mds...)
×
301
        if !m.filterApplied() {
×
302
                sortMarkdowns(m.markdowns)
×
303
        }
×
304

305
        m.updatePagination()
×
306
}
307

308
// Returns the markdowns that should be currently shown.
309
func (m stashModel) getVisibleMarkdowns() []*markdown {
×
310
        if m.filterState == filtering || m.currentSection().key == filterSection {
×
311
                return m.filteredMarkdowns
×
312
        }
×
313

314
        return m.markdowns
×
315
}
316

317
// Command for opening a markdown document in the pager. Note that this also
318
// alters the model.
319
func (m *stashModel) openMarkdown(md *markdown) tea.Cmd {
×
320
        m.viewState = stashStateLoadingDocument
×
321
        cmd := loadLocalMarkdown(md)
×
322
        return tea.Batch(cmd, m.spinner.Tick)
×
323
}
×
324

325
func (m *stashModel) hideStatusMessage() {
×
326
        m.showStatusMessage = false
×
327
        m.statusMessage = statusMessage{}
×
328
        if m.statusMessageTimer != nil {
×
329
                m.statusMessageTimer.Stop()
×
330
        }
×
331
}
332

333
func (m *stashModel) moveCursorUp() {
×
334
        m.setCursor(m.cursor() - 1)
×
335
        if m.cursor() < 0 && m.paginator().Page == 0 {
×
336
                // Stop
×
337
                m.setCursor(0)
×
338
                return
×
339
        }
×
340

341
        if m.cursor() >= 0 {
×
342
                return
×
343
        }
×
344
        // Go to previous page
345
        m.paginator().PrevPage()
×
346

×
347
        m.setCursor(m.paginator().ItemsOnPage(len(m.getVisibleMarkdowns())) - 1)
×
348
}
349

350
func (m *stashModel) moveCursorDown() {
×
351
        itemsOnPage := m.paginator().ItemsOnPage(len(m.getVisibleMarkdowns()))
×
352

×
353
        m.setCursor(m.cursor() + 1)
×
354
        if m.cursor() < itemsOnPage {
×
355
                return
×
356
        }
×
357

358
        if !m.paginator().OnLastPage() {
×
359
                m.paginator().NextPage()
×
360
                m.setCursor(0)
×
361
                return
×
362
        }
×
363

364
        // During filtering the cursor position can exceed the number of
365
        // itemsOnPage. It's more intuitive to start the cursor at the
366
        // topmost position when moving it down in this scenario.
367
        if m.cursor() > itemsOnPage {
×
368
                m.setCursor(0)
×
369
                return
×
370
        }
×
371
        m.setCursor(itemsOnPage - 1)
×
372
}
373

374
// INIT
375

376
func newStashModel(common *commonModel) stashModel {
×
377
        sp := spinner.New()
×
378
        sp.Spinner = spinner.Line
×
379
        sp.Style = stashSpinnerStyle
×
380

×
381
        si := textinput.New()
×
382
        si.Prompt = "Find:"
×
383
        si.PromptStyle = stashInputPromptStyle
×
384
        si.Cursor.Style = stashInputCursorStyle
×
385
        si.Focus()
×
386

×
387
        s := []section{
×
388
                sections[documentsSection],
×
389
        }
×
390

×
391
        m := stashModel{
×
392
                common:      common,
×
393
                spinner:     sp,
×
394
                filterInput: si,
×
395
                serverPage:  1,
×
396
                sections:    s,
×
397
        }
×
398

×
399
        return m
×
400
}
×
401

402
func newStashPaginator() paginator.Model {
×
403
        p := paginator.New()
×
404
        p.Type = paginator.Dots
×
405
        p.ActiveDot = brightGrayFg("•")
×
406
        p.InactiveDot = darkGrayFg.Render("•")
×
407
        return p
×
408
}
×
409

410
// UPDATE
411

412
func (m stashModel) update(msg tea.Msg) (stashModel, tea.Cmd) {
×
413
        var cmds []tea.Cmd
×
414

×
415
        switch msg := msg.(type) {
×
416
        case errMsg:
×
417
                m.err = msg
×
418

419
        case localFileSearchFinished:
×
420
                // We're finished searching for local files
×
421
                m.loaded = true
×
422

423
        case filteredMarkdownMsg:
×
424
                m.filteredMarkdowns = msg
×
425
                m.setCursor(0)
×
426
                return m, nil
×
427

428
        case spinner.TickMsg:
×
429
                if m.shouldSpin() {
×
430
                        var cmd tea.Cmd
×
431
                        m.spinner, cmd = m.spinner.Update(msg)
×
432
                        cmds = append(cmds, cmd)
×
433
                }
×
434

435
        case statusMessageTimeoutMsg:
×
436
                if applicationContext(msg) == stashContext {
×
437
                        m.hideStatusMessage()
×
438
                }
×
439
        }
440

441
        if m.filterState == filtering {
×
442
                cmds = append(cmds, m.handleFiltering(msg))
×
443
                return m, tea.Batch(cmds...)
×
444
        }
×
445

446
        // Updates per the current state
NEW
447
        switch m.viewState { //nolint:exhaustive
×
448
        case stashStateReady:
×
449
                cmds = append(cmds, m.handleDocumentBrowsing(msg))
×
450
        case stashStateShowingError:
×
451
                // Any key exists the error view
×
452
                if _, ok := msg.(tea.KeyMsg); ok {
×
453
                        m.viewState = stashStateReady
×
454
                }
×
455
        }
456

457
        return m, tea.Batch(cmds...)
×
458
}
459

460
// Updates for when a user is browsing the markdown listing.
461
func (m *stashModel) handleDocumentBrowsing(msg tea.Msg) tea.Cmd {
×
462
        var cmds []tea.Cmd
×
463

×
464
        numDocs := len(m.getVisibleMarkdowns())
×
465

×
466
        switch msg := msg.(type) {
×
467
        // Handle keys
468
        case tea.KeyMsg:
×
469
                switch msg.String() {
×
470
                case "k", "ctrl+k", "up":
×
471
                        m.moveCursorUp()
×
472

473
                case "j", "ctrl+j", "down":
×
474
                        m.moveCursorDown()
×
475

476
                // Go to the very start
477
                case "home", "g":
×
478
                        m.paginator().Page = 0
×
479
                        m.setCursor(0)
×
480

481
                // Go to the very end
482
                case "end", "G":
×
483
                        m.paginator().Page = m.paginator().TotalPages - 1
×
484
                        m.setCursor(m.paginator().ItemsOnPage(numDocs) - 1)
×
485

486
                // Clear filter (if applicable)
487
                case keyEsc:
×
488
                        if m.filterApplied() {
×
489
                                m.resetFiltering()
×
490
                        }
×
491

492
                // Next section
493
                case "tab", "L":
×
494
                        if len(m.sections) == 0 || m.filterState == filtering {
×
495
                                break
×
496
                        }
497
                        m.sectionIndex++
×
498
                        if m.sectionIndex >= len(m.sections) {
×
499
                                m.sectionIndex = 0
×
500
                        }
×
501
                        m.updatePagination()
×
502

503
                // Previous section
504
                case "shift+tab", "H":
×
505
                        if len(m.sections) == 0 || m.filterState == filtering {
×
506
                                break
×
507
                        }
508
                        m.sectionIndex--
×
509
                        if m.sectionIndex < 0 {
×
510
                                m.sectionIndex = len(m.sections) - 1
×
511
                        }
×
512
                        m.updatePagination()
×
513

514
                case "F":
×
515
                        m.loaded = false
×
516
                        return findLocalFiles(*m.common)
×
517

518
                // Edit document in EDITOR
519
                case "e":
×
520
                        md := m.selectedMarkdown()
×
521
                        return openEditor(md.localPath, 0)
×
522

523
                // Open document
524
                case keyEnter:
×
525
                        m.hideStatusMessage()
×
526

×
527
                        if numDocs == 0 {
×
528
                                break
×
529
                        }
530

531
                        // Load the document from the server. We'll handle the message
532
                        // that comes back in the main update function.
533
                        md := m.selectedMarkdown()
×
534
                        cmds = append(cmds, m.openMarkdown(md))
×
535

536
                // Filter your notes
537
                case "/":
×
538
                        m.hideStatusMessage()
×
539

×
540
                        // Build values we'll filter against
×
541
                        for _, md := range m.markdowns {
×
542
                                md.buildFilterValue()
×
543
                        }
×
544

545
                        m.filteredMarkdowns = m.markdowns
×
546

×
547
                        m.paginator().Page = 0
×
548
                        m.setCursor(0)
×
549
                        m.filterState = filtering
×
550
                        m.filterInput.CursorEnd()
×
551
                        m.filterInput.Focus()
×
552
                        return textinput.Blink
×
553

554
                // Toggle full help
555
                case "?":
×
556
                        m.showFullHelp = !m.showFullHelp
×
557
                        m.updatePagination()
×
558

559
                // Show errors
560
                case "!":
×
561
                        if m.err != nil && m.viewState == stashStateReady {
×
562
                                m.viewState = stashStateShowingError
×
563
                                return nil
×
564
                        }
×
565
                }
566
        }
567

568
        // Update paginator. Pagination key handling is done here, but it could
569
        // also be moved up to this level, in which case we'd use model methods
570
        // like model.PageUp().
571
        newPaginatorModel, cmd := m.paginator().Update(msg)
×
572
        m.setPaginator(newPaginatorModel)
×
573
        cmds = append(cmds, cmd)
×
574

×
575
        // Extra paginator keystrokes
×
576
        if key, ok := msg.(tea.KeyMsg); ok {
×
577
                switch key.String() {
×
578
                case "b", "u":
×
579
                        m.paginator().PrevPage()
×
580
                case "f", "d":
×
581
                        m.paginator().NextPage()
×
582
                }
583
        }
584

585
        // Keep the index in bounds when paginating
586
        itemsOnPage := m.paginator().ItemsOnPage(len(m.getVisibleMarkdowns()))
×
587
        if m.cursor() > itemsOnPage-1 {
×
588
                m.setCursor(max(0, itemsOnPage-1))
×
589
        }
×
590

591
        return tea.Batch(cmds...)
×
592
}
593

594
// Updates for when a user is in the filter editing interface.
595
func (m *stashModel) handleFiltering(msg tea.Msg) tea.Cmd {
×
596
        var cmds []tea.Cmd
×
597

×
598
        // Handle keys
×
NEW
599
        if msg, ok := msg.(tea.KeyMsg); ok { //nolint:nestif
×
600
                switch msg.String() {
×
601
                case keyEsc:
×
602
                        // Cancel filtering
×
603
                        m.resetFiltering()
×
604
                case keyEnter, "tab", "shift+tab", "ctrl+k", "up", "ctrl+j", "down":
×
605
                        m.hideStatusMessage()
×
606

×
607
                        if len(m.markdowns) == 0 {
×
608
                                break
×
609
                        }
610

611
                        h := m.getVisibleMarkdowns()
×
612

×
613
                        // If we've filtered down to nothing, clear the filter
×
614
                        if len(h) == 0 {
×
615
                                m.viewState = stashStateReady
×
616
                                m.resetFiltering()
×
617
                                break
×
618
                        }
619

620
                        // When there's only one filtered markdown left we can just
621
                        // "open" it directly
622
                        if len(h) == 1 {
×
623
                                m.viewState = stashStateReady
×
624
                                m.resetFiltering()
×
625
                                cmds = append(cmds, m.openMarkdown(h[0]))
×
626
                                break
×
627
                        }
628

629
                        // Add new section if it's not present
630
                        if m.sections[len(m.sections)-1].key != filterSection {
×
631
                                m.sections = append(m.sections, sections[filterSection])
×
632
                        }
×
633
                        m.sectionIndex = len(m.sections) - 1
×
634

×
635
                        m.filterInput.Blur()
×
636

×
637
                        m.filterState = filterApplied
×
638
                        if m.filterInput.Value() == "" {
×
639
                                m.resetFiltering()
×
640
                        }
×
641
                }
642
        }
643

644
        // Update the filter text input component
645
        newFilterInputModel, inputCmd := m.filterInput.Update(msg)
×
646
        currentFilterVal := m.filterInput.Value()
×
647
        newFilterVal := newFilterInputModel.Value()
×
648
        m.filterInput = newFilterInputModel
×
649
        cmds = append(cmds, inputCmd)
×
650

×
651
        // If the filtering input has changed, request updated filtering
×
652
        if newFilterVal != currentFilterVal {
×
653
                cmds = append(cmds, filterMarkdowns(*m))
×
654
        }
×
655

656
        // Update pagination
657
        m.updatePagination()
×
658

×
659
        return tea.Batch(cmds...)
×
660
}
661

662
// VIEW
663

664
func (m stashModel) view() string {
×
665
        var s string
×
666
        switch m.viewState {
×
667
        case stashStateShowingError:
×
668
                return errorView(m.err, false)
×
669
        case stashStateLoadingDocument:
×
670
                s += " " + m.spinner.View() + " Loading document..."
×
671
        case stashStateReady:
×
672
                loadingIndicator := " "
×
673
                if m.shouldSpin() {
×
674
                        loadingIndicator = m.spinner.View()
×
675
                }
×
676

677
                // Only draw the normal header if we're not using the header area for
678
                // something else (like a note or delete prompt).
679
                header := m.headerView()
×
680

×
681
                // Rules for the logo, filter and status message.
×
682
                logoOrFilter := " "
×
683
                if m.showStatusMessage && m.filterState == filtering {
×
684
                        logoOrFilter += m.statusMessage.String()
×
685
                } else if m.filterState == filtering {
×
686
                        logoOrFilter += m.filterInput.View()
×
687
                } else {
×
688
                        logoOrFilter += glowLogoView()
×
689
                        if m.showStatusMessage {
×
690
                                logoOrFilter += "  " + m.statusMessage.String()
×
691
                        }
×
692
                }
NEW
693
                logoOrFilter = truncate.StringWithTail(logoOrFilter, uint(m.common.width-1), ellipsis) //nolint:gosec
×
694

×
695
                help, helpHeight := m.helpView()
×
696

×
697
                populatedView := m.populatedView()
×
698
                populatedViewHeight := strings.Count(populatedView, "\n") + 2
×
699

×
700
                // We need to fill any empty height with newlines so the footer reaches
×
701
                // the bottom.
×
702
                availHeight := m.common.height -
×
703
                        stashViewTopPadding -
×
704
                        populatedViewHeight -
×
705
                        helpHeight -
×
706
                        stashViewBottomPadding
×
707
                blankLines := strings.Repeat("\n", max(0, availHeight))
×
708

×
709
                var pagination string
×
710
                if m.paginator().TotalPages > 1 {
×
711
                        pagination = m.paginator().View()
×
712

×
713
                        // If the dot pagination is wider than the width of the window
×
714
                        // use the arabic paginator.
×
715
                        if ansi.PrintableRuneWidth(pagination) > m.common.width-stashViewHorizontalPadding {
×
716
                                // Copy the paginator since m.paginator() returns a pointer to
×
717
                                // the active paginator and we don't want to mutate it. In
×
718
                                // normal cases, where the paginator is not a pointer, we could
×
719
                                // safely change the model parameters for rendering here as the
×
720
                                // current model is discarded after reuturning from a View().
×
721
                                // One could argue, in fact, that using pointers in
×
722
                                // a functional framework is an antipattern and our use of
×
723
                                // pointers in our model should be refactored away.
×
724
                                p := *(m.paginator())
×
725
                                p.Type = paginator.Arabic
×
726
                                pagination = paginationStyle.Render(p.View())
×
727
                        }
×
728
                }
729

730
                s += fmt.Sprintf(
×
731
                        "%s%s\n\n  %s\n\n%s\n\n%s  %s\n\n%s",
×
732
                        loadingIndicator,
×
733
                        logoOrFilter,
×
734
                        header,
×
735
                        populatedView,
×
736
                        blankLines,
×
737
                        pagination,
×
738
                        help,
×
739
                )
×
740
        }
741
        return "\n" + indent(s, stashIndent)
×
742
}
743

744
func glowLogoView() string {
×
745
        return logoStyle.Render(" Glow ")
×
746
}
×
747

748
func (m stashModel) headerView() string {
×
749
        localCount := len(m.markdowns)
×
750

×
751
        var sections []string //nolint:prealloc
×
752

×
753
        // Filter results
×
754
        if m.filterState == filtering {
×
755
                if localCount == 0 {
×
756
                        return grayFg("Nothing found.")
×
757
                }
×
758
                if localCount > 0 {
×
759
                        sections = append(sections, fmt.Sprintf("%d local", localCount))
×
760
                }
×
761

762
                for i := range sections {
×
763
                        sections[i] = grayFg(sections[i])
×
764
                }
×
765

766
                return strings.Join(sections, dividerDot.String())
×
767
        }
768

769
        // Tabs
770
        for i, v := range m.sections {
×
771
                var s string
×
772

×
773
                switch v.key {
×
774
                case documentsSection:
×
775
                        s = fmt.Sprintf("%d documents", localCount)
×
776

777
                case filterSection:
×
778
                        s = fmt.Sprintf("%d ā€œ%sā€", len(m.filteredMarkdowns), m.filterInput.Value())
×
779
                }
780

781
                if m.sectionIndex == i && len(m.sections) > 1 {
×
782
                        s = selectedTabStyle.Render(s)
×
783
                } else {
×
784
                        s = tabStyle.Render(s)
×
785
                }
×
786
                sections = append(sections, s)
×
787
        }
788

789
        return strings.Join(sections, dividerBar.String())
×
790
}
791

792
func (m stashModel) populatedView() string {
×
793
        mds := m.getVisibleMarkdowns()
×
794

×
795
        var b strings.Builder
×
796

×
797
        // Empty states
×
798
        if len(mds) == 0 {
×
799
                f := func(s string) {
×
800
                        b.WriteString("  " + grayFg(s))
×
801
                }
×
802

803
                switch m.sections[m.sectionIndex].key {
×
804
                case documentsSection:
×
805
                        if m.loadingDone() {
×
806
                                f("No files found.")
×
807
                        } else {
×
808
                                f("Looking for local files...")
×
809
                        }
×
810
                case filterSection:
×
811
                        return ""
×
812
                }
813
        }
814

815
        if len(mds) > 0 {
×
816
                start, end := m.paginator().GetSliceBounds(len(mds))
×
817
                docs := mds[start:end]
×
818

×
819
                for i, md := range docs {
×
820
                        stashItemView(&b, m, i, md)
×
821
                        if i != len(docs)-1 {
×
822
                                fmt.Fprintf(&b, "\n\n")
×
823
                        }
×
824
                }
825
        }
826

827
        // If there aren't enough items to fill up this page (always the last page)
828
        // then we need to add some newlines to fill up the space where stash items
829
        // would have been.
830
        itemsOnPage := m.paginator().ItemsOnPage(len(mds))
×
831
        if itemsOnPage < m.paginator().PerPage {
×
832
                n := (m.paginator().PerPage - itemsOnPage) * stashViewItemHeight
×
833
                if len(mds) == 0 {
×
834
                        n -= stashViewItemHeight - 1
×
835
                }
×
836
                for i := 0; i < n; i++ {
×
837
                        fmt.Fprint(&b, "\n")
×
838
                }
×
839
        }
840

841
        return b.String()
×
842
}
843

844
// COMMANDS
845

846
func loadLocalMarkdown(md *markdown) tea.Cmd {
×
847
        return func() tea.Msg {
×
848
                if md.localPath == "" {
×
849
                        return errMsg{errors.New("could not load file: missing path")}
×
850
                }
×
851

852
                data, err := os.ReadFile(md.localPath)
×
853
                if err != nil {
×
854
                        log.Debug("error reading local file", "error", err)
×
855
                        return errMsg{err}
×
856
                }
×
857
                md.Body = string(data)
×
858
                return fetchedMarkdownMsg(md)
×
859
        }
860
}
861

862
func filterMarkdowns(m stashModel) tea.Cmd {
×
863
        return func() tea.Msg {
×
864
                if m.filterInput.Value() == "" || !m.filterApplied() {
×
865
                        return filteredMarkdownMsg(m.markdowns) // return everything
×
866
                }
×
867

868
                targets := []string{}
×
869
                mds := m.markdowns
×
870

×
871
                for _, t := range mds {
×
872
                        targets = append(targets, t.filterValue)
×
873
                }
×
874

875
                ranks := fuzzy.Find(m.filterInput.Value(), targets)
×
876
                sort.Stable(ranks)
×
877

×
878
                filtered := []*markdown{}
×
879
                for _, r := range ranks {
×
880
                        filtered = append(filtered, mds[r.Index])
×
881
                }
×
882

883
                return filteredMarkdownMsg(filtered)
×
884
        }
885
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc