Compare commits

...

176 commits

Author SHA1 Message Date
Drew Smirnoff
e5cb757278
chore: govulncheck bumps (#1004)
Signed-off-by: drew <me@andrinoff.com>
2026-08-04 18:41:27 +04:00
Andrey Nering
53788271b3 chore: remove CODEOWNERS 2026-04-22 11:14:50 -03:00
Andrey Nering
f57087499b
v2.1.2 2026-04-09 17:25:27 -03:00
Andrey Nering
1130f2d89f
ci: fix govulncheck (#927) 2026-04-09 16:18:13 -03:00
Matt Van Horn
fac0fb456d
fix: use shell.Fields to parse $PAGER command (#922)
Replace cmd/sh shell invocation with shell.Fields from mvdan.cc/sh.
This properly handles paths with spaces on all platforms without
shelling out.

Fixes #385
2026-04-09 16:12:10 -03:00
dependabot[bot]
752de97c5a
chore(deps): bump the all group across 1 directory with 4 updates (#857)
Bumps the all group with 4 updates in the / directory: [github.com/spf13/cobra](https://github.com/spf13/cobra), [golang.org/x/sys](https://github.com/golang/sys), [golang.org/x/term](https://github.com/golang/term) and [golang.org/x/text](https://github.com/golang/text).


Updates `github.com/spf13/cobra` from 1.10.1 to 1.10.2
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/v1.10.1...v1.10.2)

Updates `golang.org/x/sys` from 0.38.0 to 0.39.0
- [Commits](https://github.com/golang/sys/compare/v0.38.0...v0.39.0)

Updates `golang.org/x/term` from 0.37.0 to 0.38.0
- [Commits](https://github.com/golang/term/compare/v0.37.0...v0.38.0)

Updates `golang.org/x/text` from 0.31.0 to 0.32.0
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.31.0...v0.32.0)

---
updated-dependencies:
- dependency-name: github.com/spf13/cobra
  dependency-version: 1.10.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: golang.org/x/sys
  dependency-version: 0.39.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all
- dependency-name: golang.org/x/term
  dependency-version: 0.38.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all
- dependency-name: golang.org/x/text
  dependency-version: 0.32.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-15 10:08:33 +00:00
dependabot[bot]
7903cdbc5f
chore(deps): bump golangci/golangci-lint-action in the all group (#854)
Bumps the all group with 1 update: [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action).


Updates `golangci/golangci-lint-action` from 9.1.0 to 9.2.0
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v9.1.0...v9.2.0)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-version: 9.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-08 10:05:33 +00:00
dependabot[bot]
a861530349
chore(deps): bump the all group with 2 updates (#848)
Bumps the all group with 2 updates: [actions/checkout](https://github.com/actions/checkout) and [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action).


Updates `actions/checkout` from 5 to 6
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

Updates `golangci/golangci-lint-action` from 9.0.0 to 9.1.0
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v9.0.0...v9.1.0)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: all
- dependency-name: golangci/golangci-lint-action
  dependency-version: 9.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-24 11:33:59 +00:00
dependabot[bot]
ba37804fd5
chore(deps): bump the all group with 2 updates (#847)
Bumps the all group with 2 updates: [golang.org/x/term](https://github.com/golang/term) and [golang.org/x/text](https://github.com/golang/text).


Updates `golang.org/x/term` from 0.36.0 to 0.37.0
- [Commits](https://github.com/golang/term/compare/v0.36.0...v0.37.0)

Updates `golang.org/x/text` from 0.30.0 to 0.31.0
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.30.0...v0.31.0)

---
updated-dependencies:
- dependency-name: golang.org/x/term
  dependency-version: 0.37.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all
- dependency-name: golang.org/x/text
  dependency-version: 0.31.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-17 10:43:05 +00:00
dependabot[bot]
dbf59aafa9
chore(deps): bump golang.org/x/sys in the all group (#844)
Bumps the all group with 1 update: [golang.org/x/sys](https://github.com/golang/sys).


Updates `golang.org/x/sys` from 0.37.0 to 0.38.0
- [Commits](https://github.com/golang/sys/compare/v0.37.0...v0.38.0)

---
updated-dependencies:
- dependency-name: golang.org/x/sys
  dependency-version: 0.38.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-10 11:35:26 +00:00
dependabot[bot]
07d4e1de3f
chore(deps): bump golangci/golangci-lint-action in the all group (#843)
Bumps the all group with 1 update: [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action).


Updates `golangci/golangci-lint-action` from 8.0.0 to 9.0.0
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v8.0.0...v9.0.0)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-version: 9.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: all
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-10 11:33:09 +00:00
dependabot[bot]
e9840f0735
chore(deps): bump the all group with 3 updates (#833)
Bumps the all group with 3 updates: [golang.org/x/sys](https://github.com/golang/sys), [golang.org/x/term](https://github.com/golang/term) and [golang.org/x/text](https://github.com/golang/text).


Updates `golang.org/x/sys` from 0.36.0 to 0.37.0
- [Commits](https://github.com/golang/sys/compare/v0.36.0...v0.37.0)

Updates `golang.org/x/term` from 0.35.0 to 0.36.0
- [Commits](https://github.com/golang/term/compare/v0.35.0...v0.36.0)

Updates `golang.org/x/text` from 0.29.0 to 0.30.0
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.29.0...v0.30.0)

---
updated-dependencies:
- dependency-name: golang.org/x/sys
  dependency-version: 0.37.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all
- dependency-name: golang.org/x/term
  dependency-version: 0.36.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all
- dependency-name: golang.org/x/text
  dependency-version: 0.30.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-13 09:24:50 +00:00
dependabot[bot]
283d20b83c
chore(deps): bump github.com/mattn/go-runewidth in the all group (#832)
Bumps the all group with 1 update: [github.com/mattn/go-runewidth](https://github.com/mattn/go-runewidth).


Updates `github.com/mattn/go-runewidth` from 0.0.17 to 0.0.19
- [Commits](https://github.com/mattn/go-runewidth/compare/v0.0.17...v0.0.19)

---
updated-dependencies:
- dependency-name: github.com/mattn/go-runewidth
  dependency-version: 0.0.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-05 15:39:22 +00:00
Charm
36bf1711bf
ci: sync dependabot config (#831) 2025-10-05 17:30:25 +02:00
dependabot[bot]
f9ec6882c1
chore(deps): bump github.com/mattn/go-runewidth in the all group (#828)
Bumps the all group with 1 update: [github.com/mattn/go-runewidth](https://github.com/mattn/go-runewidth).


Updates `github.com/mattn/go-runewidth` from 0.0.16 to 0.0.17
- [Commits](https://github.com/mattn/go-runewidth/compare/v0.0.16...v0.0.17)

---
updated-dependencies:
- dependency-name: github.com/mattn/go-runewidth
  dependency-version: 0.0.17
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-29 10:41:25 +00:00
dependabot[bot]
1afeed4c56
chore(deps): bump github.com/charmbracelet/bubbletea in the all group (#827)
Bumps the all group with 1 update: [github.com/charmbracelet/bubbletea](https://github.com/charmbracelet/bubbletea).


Updates `github.com/charmbracelet/bubbletea` from 1.3.9 to 1.3.10
- [Release notes](https://github.com/charmbracelet/bubbletea/releases)
- [Changelog](https://github.com/charmbracelet/bubbletea/blob/main/.goreleaser.yml)
- [Commits](https://github.com/charmbracelet/bubbletea/compare/v1.3.9...v1.3.10)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/bubbletea
  dependency-version: 1.3.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-22 09:14:29 +00:00
Nathan Cain
677fcb3eee bump install with go path to v2 2025-09-18 22:11:28 -04:00
dependabot[bot]
a440b3319b
chore(deps): bump the all group with 3 updates (#824)
Bumps the all group with 3 updates: [github.com/charmbracelet/bubbletea](https://github.com/charmbracelet/bubbletea), [github.com/muesli/mango-cobra](https://github.com/muesli/mango-cobra) and [github.com/spf13/viper](https://github.com/spf13/viper).


Updates `github.com/charmbracelet/bubbletea` from 1.3.7 to 1.3.9
- [Release notes](https://github.com/charmbracelet/bubbletea/releases)
- [Changelog](https://github.com/charmbracelet/bubbletea/blob/main/.goreleaser.yml)
- [Commits](https://github.com/charmbracelet/bubbletea/compare/v1.3.7...v1.3.9)

Updates `github.com/muesli/mango-cobra` from 1.2.0 to 1.3.0
- [Release notes](https://github.com/muesli/mango-cobra/releases)
- [Commits](https://github.com/muesli/mango-cobra/compare/v1.2.0...v1.3.0)

Updates `github.com/spf13/viper` from 1.20.1 to 1.21.0
- [Release notes](https://github.com/spf13/viper/releases)
- [Commits](https://github.com/spf13/viper/compare/v1.20.1...v1.21.0)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/bubbletea
  dependency-version: 1.3.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: github.com/muesli/mango-cobra
  dependency-version: 1.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all
- dependency-name: github.com/spf13/viper
  dependency-version: 1.21.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-15 09:24:52 +00:00
dependabot[bot]
1c32d1377c
chore(deps): bump actions/setup-go from 5 to 6 in the all group (#821)
Bumps the all group with 1 update: [actions/setup-go](https://github.com/actions/setup-go).


Updates `actions/setup-go` from 5 to 6
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: all
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-09 10:14:37 -04:00
Andrey Nering
963219b96e
security: bump mapstructure module with security fixes (#822) 2025-09-08 09:35:05 -03:00
dependabot[bot]
901b99931e
chore(deps): bump the all group with 5 updates (#820)
Bumps the all group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [github.com/charmbracelet/bubbletea](https://github.com/charmbracelet/bubbletea) | `1.3.6` | `1.3.7` |
| [github.com/spf13/cobra](https://github.com/spf13/cobra) | `1.9.1` | `1.10.1` |
| [golang.org/x/sys](https://github.com/golang/sys) | `0.35.0` | `0.36.0` |
| [golang.org/x/term](https://github.com/golang/term) | `0.34.0` | `0.35.0` |
| [golang.org/x/text](https://github.com/golang/text) | `0.28.0` | `0.29.0` |


Updates `github.com/charmbracelet/bubbletea` from 1.3.6 to 1.3.7
- [Release notes](https://github.com/charmbracelet/bubbletea/releases)
- [Changelog](https://github.com/charmbracelet/bubbletea/blob/main/.goreleaser.yml)
- [Commits](https://github.com/charmbracelet/bubbletea/compare/v1.3.6...v1.3.7)

Updates `github.com/spf13/cobra` from 1.9.1 to 1.10.1
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/v1.9.1...v1.10.1)

Updates `golang.org/x/sys` from 0.35.0 to 0.36.0
- [Commits](https://github.com/golang/sys/compare/v0.35.0...v0.36.0)

Updates `golang.org/x/term` from 0.34.0 to 0.35.0
- [Commits](https://github.com/golang/term/compare/v0.34.0...v0.35.0)

Updates `golang.org/x/text` from 0.28.0 to 0.29.0
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.28.0...v0.29.0)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/bubbletea
  dependency-version: 1.3.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all
- dependency-name: github.com/spf13/cobra
  dependency-version: 1.10.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all
- dependency-name: golang.org/x/sys
  dependency-version: 0.36.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all
- dependency-name: golang.org/x/term
  dependency-version: 0.35.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all
- dependency-name: golang.org/x/text
  dependency-version: 0.29.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-08 09:30:47 -03:00
dependabot[bot]
9432fd920c
chore(deps): bump the all group with 3 updates (#818)
Bumps the all group with 3 updates: [golang.org/x/sys](https://github.com/golang/sys), [golang.org/x/term](https://github.com/golang/term) and [golang.org/x/text](https://github.com/golang/text).


Updates `golang.org/x/sys` from 0.34.0 to 0.35.0
- [Commits](https://github.com/golang/sys/compare/v0.34.0...v0.35.0)

Updates `golang.org/x/term` from 0.33.0 to 0.34.0
- [Commits](https://github.com/golang/term/compare/v0.33.0...v0.34.0)

Updates `golang.org/x/text` from 0.27.0 to 0.28.0
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.27.0...v0.28.0)

---
updated-dependencies:
- dependency-name: golang.org/x/sys
  dependency-version: 0.35.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all
- dependency-name: golang.org/x/term
  dependency-version: 0.34.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all
- dependency-name: golang.org/x/text
  dependency-version: 0.28.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-27 13:33:59 -03:00
Charm
deaa57fe00
ci: sync dependabot config (#817) 2025-08-26 13:04:08 -03:00
dependabot[bot]
e61f329428
chore(deps): bump actions/checkout from 4 to 5 (#813)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-18 10:55:11 -03:00
github-actions[bot]
bf0a9b4d3d
ci: sync golangci-lint config (#808)
Co-authored-by: andreynering <7011819+andreynering@users.noreply.github.com>
2025-08-11 08:23:51 -04:00
dependabot[bot]
e010ed866e
chore(deps): bump golang.org/x/term from 0.32.0 to 0.33.0 (#798)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.32.0 to 0.33.0.
- [Commits](https://github.com/golang/term/compare/v0.32.0...v0.33.0)

---
updated-dependencies:
- dependency-name: golang.org/x/term
  dependency-version: 0.33.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-14 12:23:28 +00:00
dependabot[bot]
0e76788a6a
chore(deps): bump github.com/charmbracelet/bubbletea from 1.3.5 to 1.3.6 (#797)
Bumps [github.com/charmbracelet/bubbletea](https://github.com/charmbracelet/bubbletea) from 1.3.5 to 1.3.6.
- [Release notes](https://github.com/charmbracelet/bubbletea/releases)
- [Changelog](https://github.com/charmbracelet/bubbletea/blob/main/.goreleaser.yml)
- [Commits](https://github.com/charmbracelet/bubbletea/compare/v1.3.5...v1.3.6)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/bubbletea
  dependency-version: 1.3.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-14 12:20:32 +00:00
dependabot[bot]
66b389ef17
chore(deps): bump golang.org/x/sys from 0.33.0 to 0.34.0 (#796)
Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.33.0 to 0.34.0.
- [Commits](https://github.com/golang/sys/compare/v0.33.0...v0.34.0)

---
updated-dependencies:
- dependency-name: golang.org/x/sys
  dependency-version: 0.34.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-14 11:50:28 +00:00
dependabot[bot]
18a739c091
chore(deps): bump golang.org/x/text from 0.26.0 to 0.27.0 (#795)
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.26.0 to 0.27.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.26.0...v0.27.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-version: 0.27.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-14 11:24:38 +00:00
one230six
9240ab4625
docs: remove redundant word in comment (#782) 2025-06-12 10:25:33 -03:00
bashbunni
a6ed0f2430
docs: add contribution guidelines (#781) 2025-06-11 08:16:46 -03:00
dependabot[bot]
3079ce20b9
chore(deps): bump golang.org/x/text from 0.25.0 to 0.26.0 (#779)
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.25.0 to 0.26.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.25.0...v0.26.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-version: 0.26.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-09 09:49:18 +00:00
Prithvi Jethwa
bcd1296ce5
fix: allow config file to load showLineNumbers (#653) 2025-06-04 14:41:27 -03:00
github-actions[bot]
e54618ef13
ci: sync golangci-lint config (#777)
Co-authored-by: andreynering <7011819+andreynering@users.noreply.github.com>
2025-06-02 08:52:12 -03:00
Carlos Alexandro Becker
d37e988787
chore(deps): update x/net
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2025-05-30 09:58:41 -03:00
dependabot[bot]
e83c6edf93
chore(deps): bump github.com/charmbracelet/log from 0.4.1 to 0.4.2 (#769)
Bumps [github.com/charmbracelet/log](https://github.com/charmbracelet/log) from 0.4.1 to 0.4.2.
- [Release notes](https://github.com/charmbracelet/log/releases)
- [Commits](https://github.com/charmbracelet/log/compare/v0.4.1...v0.4.2)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/log
  dependency-version: 0.4.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-19 09:08:18 +00:00
Markus Billharz
e23f237fb7 fix: only show edit if documents are available 2025-05-14 21:32:15 -04:00
Markus Billharz
ee27edd3b1 fix: handle case when selected md is nil 2025-05-14 21:32:15 -04:00
dependabot[bot]
5b2dfddcda
chore(deps): bump golang.org/x/term from 0.31.0 to 0.32.0 (#762)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.31.0 to 0.32.0.
- [Commits](https://github.com/golang/term/compare/v0.31.0...v0.32.0)

---
updated-dependencies:
- dependency-name: golang.org/x/term
  dependency-version: 0.32.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-12 09:25:09 +00:00
dependabot[bot]
0880dea1be
chore(deps): bump golang.org/x/text from 0.24.0 to 0.25.0 (#763)
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.24.0 to 0.25.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.24.0...v0.25.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-version: 0.25.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-12 09:21:41 +00:00
dependabot[bot]
ef85822a00
chore(deps): bump golang.org/x/sys from 0.32.0 to 0.33.0 (#764)
Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.32.0 to 0.33.0.
- [Commits](https://github.com/golang/sys/compare/v0.32.0...v0.33.0)

---
updated-dependencies:
- dependency-name: golang.org/x/sys
  dependency-version: 0.33.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-12 09:21:02 +00:00
dependabot[bot]
cdeceb9a7f
chore(deps): bump golangci/golangci-lint-action from 7.0.0 to 8.0.0 (#759)
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 7.0.0 to 8.0.0.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v7.0.0...v8.0.0)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-version: 8.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-05 10:22:45 +00:00
dependabot[bot]
366e9a0d65
chore(deps): bump github.com/charmbracelet/bubbletea from 1.3.4 to 1.3.5 (#758)
Bumps [github.com/charmbracelet/bubbletea](https://github.com/charmbracelet/bubbletea) from 1.3.4 to 1.3.5.
- [Release notes](https://github.com/charmbracelet/bubbletea/releases)
- [Changelog](https://github.com/charmbracelet/bubbletea/blob/main/.goreleaser.yml)
- [Commits](https://github.com/charmbracelet/bubbletea/compare/v1.3.4...v1.3.5)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/bubbletea
  dependency-version: 1.3.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-05 09:45:24 +00:00
Ktrod
76e08e7dce
fix: sync viewport on half page up/down to prevent duplicate strings in render (#756) 2025-04-28 10:29:49 -03:00
Charm
e60e83c6aa
ci: sync dependabot config (#755) 2025-04-28 08:46:31 -03:00
dependabot[bot]
bb9fcb439b
chore(deps): bump github.com/charmbracelet/glamour from 0.9.1 to 0.10.0 (#753)
Bumps [github.com/charmbracelet/glamour](https://github.com/charmbracelet/glamour) from 0.9.1 to 0.10.0.
- [Release notes](https://github.com/charmbracelet/glamour/releases)
- [Changelog](https://github.com/charmbracelet/glamour/blob/master/.goreleaser.yml)
- [Commits](https://github.com/charmbracelet/glamour/compare/v0.9.1...v0.10.0)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/glamour
  dependency-version: 0.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-21 10:47:08 +00:00
Andrey Nering
6303d0b784
chore(deps): update golang.org/x/net with security fixes (#749) 2025-04-17 10:38:19 -03:00
Andrey Nering
d6b9ddf345
docs: mention snapcraft release on readme (#748)
Closes #747
2025-04-17 10:08:27 -03:00
dependabot[bot]
470fa3d718
chore(deps): bump golang.org/x/text from 0.23.0 to 0.24.0 (#742)
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.23.0 to 0.24.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.23.0...v0.24.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-version: 0.24.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-14 09:51:09 +00:00
dependabot[bot]
2c0e4d878a
chore(deps): bump github.com/charmbracelet/bubbles from 0.20.0 to 0.21.0 (#746)
Bumps [github.com/charmbracelet/bubbles](https://github.com/charmbracelet/bubbles) from 0.20.0 to 0.21.0.
- [Release notes](https://github.com/charmbracelet/bubbles/releases)
- [Changelog](https://github.com/charmbracelet/bubbles/blob/master/.goreleaser.yml)
- [Commits](https://github.com/charmbracelet/bubbles/compare/v0.20.0...v0.21.0)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/bubbles
  dependency-version: 0.21.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-14 09:48:13 +00:00
Glauber Santana
d2e04adbfe
fix: tui mode handling of remote urls (#744) 2025-04-09 10:18:13 -03:00
dependabot[bot]
c84a03bbfd
chore(deps): bump github.com/fsnotify/fsnotify from 1.8.0 to 1.9.0 (#741)
Bumps [github.com/fsnotify/fsnotify](https://github.com/fsnotify/fsnotify) from 1.8.0 to 1.9.0.
- [Release notes](https://github.com/fsnotify/fsnotify/releases)
- [Changelog](https://github.com/fsnotify/fsnotify/blob/main/CHANGELOG.md)
- [Commits](https://github.com/fsnotify/fsnotify/compare/v1.8.0...v1.9.0)

---
updated-dependencies:
- dependency-name: github.com/fsnotify/fsnotify
  dependency-version: 1.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-07 09:48:44 +00:00
dependabot[bot]
e8049d79f2
chore(deps): bump golang.org/x/term from 0.30.0 to 0.31.0 (#739)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.30.0 to 0.31.0.
- [Commits](https://github.com/golang/term/compare/v0.30.0...v0.31.0)

---
updated-dependencies:
- dependency-name: golang.org/x/term
  dependency-version: 0.31.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-07 09:48:19 +00:00
Andrey Nering
41d70ee83b fix(lint): fix all linting issues 2025-04-03 15:23:59 -03:00
Andrey Nering
0a6732f089 fix: update .golangci.yml for v2 2025-04-03 15:23:59 -03:00
Andrey Nering
230dbc79b2 ci: add lint-sync workflow 2025-04-03 15:23:59 -03:00
Andrey Nering
81c64fdd2d chore: delete makefile 2025-04-03 15:23:59 -03:00
Andrey Nering
c48ee3d98c chore: add a taskfile 2025-04-03 15:23:59 -03:00
Andrey Nering
23f9873f6a chore: add .editorconfig 2025-04-03 15:23:59 -03:00
Ktrod
05872a506a
fix(watch): watch for dir instead of file to work on all scenarios (#734)
Co-authored-by: Andrey Nering <andreynering@users.noreply.github.com>
2025-04-02 09:47:04 -03:00
dependabot[bot]
50a763ef94
chore(deps): bump github.com/spf13/viper from 1.20.0 to 1.20.1 (#731)
Bumps [github.com/spf13/viper](https://github.com/spf13/viper) from 1.20.0 to 1.20.1.
- [Release notes](https://github.com/spf13/viper/releases)
- [Commits](https://github.com/spf13/viper/compare/v1.20.0...v1.20.1)

---
updated-dependencies:
- dependency-name: github.com/spf13/viper
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-31 09:49:02 +00:00
dependabot[bot]
4d59a973a7
chore(deps): bump golangci/golangci-lint-action from 6.5.2 to 7.0.0 (#730)
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 6.5.2 to 7.0.0.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v6.5.2...v7.0.0)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-31 09:21:02 +00:00
dependabot[bot]
d54666ddf7
chore(deps): bump github.com/charmbracelet/glamour from 0.9.0 to 0.9.1 (#727)
Bumps [github.com/charmbracelet/glamour](https://github.com/charmbracelet/glamour) from 0.9.0 to 0.9.1.
- [Release notes](https://github.com/charmbracelet/glamour/releases)
- [Changelog](https://github.com/charmbracelet/glamour/blob/master/.goreleaser.yml)
- [Commits](https://github.com/charmbracelet/glamour/compare/v0.9.0...v0.9.1)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/glamour
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-24 10:14:31 +00:00
dependabot[bot]
0b668e1593
chore(deps): bump golangci/golangci-lint-action from 6.5.1 to 6.5.2 (#726)
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 6.5.1 to 6.5.2.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v6.5.1...v6.5.2)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-24 09:44:16 +00:00
dependabot[bot]
6558aaab69
chore(deps): bump github.com/charmbracelet/log from 0.4.0 to 0.4.1 (#724)
Bumps [github.com/charmbracelet/log](https://github.com/charmbracelet/log) from 0.4.0 to 0.4.1.
- [Release notes](https://github.com/charmbracelet/log/releases)
- [Commits](https://github.com/charmbracelet/log/compare/v0.4.0...v0.4.1)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/log
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-17 10:15:16 +00:00
dependabot[bot]
85668a6a21
chore(deps): bump github.com/spf13/viper from 1.19.0 to 1.20.0 (#722)
Bumps [github.com/spf13/viper](https://github.com/spf13/viper) from 1.19.0 to 1.20.0.
- [Release notes](https://github.com/spf13/viper/releases)
- [Commits](https://github.com/spf13/viper/compare/v1.19.0...v1.20.0)

---
updated-dependencies:
- dependency-name: github.com/spf13/viper
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-17 10:08:56 +00:00
dependabot[bot]
411cdb142f
chore(deps): bump github.com/charmbracelet/glamour from 0.8.0 to 0.9.0 (#721)
Bumps [github.com/charmbracelet/glamour](https://github.com/charmbracelet/glamour) from 0.8.0 to 0.9.0.
- [Release notes](https://github.com/charmbracelet/glamour/releases)
- [Changelog](https://github.com/charmbracelet/glamour/blob/master/.goreleaser.yml)
- [Commits](https://github.com/charmbracelet/glamour/compare/v0.8.0...v0.9.0)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/glamour
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-17 10:08:02 +00:00
dependabot[bot]
6cd480d68d
chore(deps): bump golangci/golangci-lint-action from 6.5.0 to 6.5.1 (#720)
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 6.5.0 to 6.5.1.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v6.5.0...v6.5.1)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-17 09:31:38 +00:00
dependabot[bot]
652010599b
chore(deps): bump golang.org/x/text from 0.22.0 to 0.23.0 (#715)
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.22.0 to 0.23.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.22.0...v0.23.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-10 10:14:22 +00:00
dependabot[bot]
eeb0fdd06f
chore(deps): bump golang.org/x/term from 0.29.0 to 0.30.0 (#714)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.29.0 to 0.30.0.
- [Commits](https://github.com/golang/term/compare/v0.29.0...v0.30.0)

---
updated-dependencies:
- dependency-name: golang.org/x/term
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-10 10:10:35 +00:00
Carlos Alexandro Becker
f88a901caa
ci: fix goreleaser config 2025-03-07 15:10:54 -03:00
dependabot[bot]
7dc4b9c87f
chore(deps): bump github.com/charmbracelet/bubbletea from 1.3.3 to 1.3.4 (#711)
Bumps [github.com/charmbracelet/bubbletea](https://github.com/charmbracelet/bubbletea) from 1.3.3 to 1.3.4.
- [Release notes](https://github.com/charmbracelet/bubbletea/releases)
- [Changelog](https://github.com/charmbracelet/bubbletea/blob/main/.goreleaser.yml)
- [Commits](https://github.com/charmbracelet/bubbletea/compare/v1.3.3...v1.3.4)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/bubbletea
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-03 10:36:24 +00:00
dependabot[bot]
2da054e802
chore(deps): bump golang.org/x/net from 0.27.0 to 0.33.0 (#707)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.27.0 to 0.33.0.
- [Commits](https://github.com/golang/net/compare/v0.27.0...v0.33.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-26 20:39:11 +00:00
dependabot[bot]
7154552fa0
chore(deps): bump github.com/muesli/termenv (#704)
Bumps [github.com/muesli/termenv](https://github.com/muesli/termenv) from 0.15.3-0.20240618155329-98d742f6907a to 0.16.0.
- [Release notes](https://github.com/muesli/termenv/releases)
- [Commits](https://github.com/muesli/termenv/commits/v0.16.0)

---
updated-dependencies:
- dependency-name: github.com/muesli/termenv
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-24 11:06:44 +00:00
dependabot[bot]
0401d55125
chore(deps): bump github.com/spf13/viper from 1.15.0 to 1.19.0 (#701)
Bumps [github.com/spf13/viper](https://github.com/spf13/viper) from 1.15.0 to 1.19.0.
- [Release notes](https://github.com/spf13/viper/releases)
- [Commits](https://github.com/spf13/viper/compare/v1.15.0...v1.19.0)

---
updated-dependencies:
- dependency-name: github.com/spf13/viper
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-17 11:28:14 +00:00
dependabot[bot]
6175a7989e
chore(deps): bump github.com/caarlos0/env/v11 from 11.0.1 to 11.3.1 (#700)
Bumps [github.com/caarlos0/env/v11](https://github.com/caarlos0/env) from 11.0.1 to 11.3.1.
- [Release notes](https://github.com/caarlos0/env/releases)
- [Changelog](https://github.com/caarlos0/env/blob/main/.goreleaser.yml)
- [Commits](https://github.com/caarlos0/env/compare/v11.0.1...v11.3.1)

---
updated-dependencies:
- dependency-name: github.com/caarlos0/env/v11
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-17 11:25:33 +00:00
dependabot[bot]
5526167854
chore(deps): bump github.com/spf13/cobra from 1.7.0 to 1.9.1 (#699)
Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 1.7.0 to 1.9.1.
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/v1.7.0...v1.9.1)

---
updated-dependencies:
- dependency-name: github.com/spf13/cobra
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-17 11:25:26 +00:00
dependabot[bot]
f0f1201ba5
chore(deps): bump github.com/fsnotify/fsnotify from 1.6.0 to 1.8.0 (#698)
Bumps [github.com/fsnotify/fsnotify](https://github.com/fsnotify/fsnotify) from 1.6.0 to 1.8.0.
- [Release notes](https://github.com/fsnotify/fsnotify/releases)
- [Changelog](https://github.com/fsnotify/fsnotify/blob/main/CHANGELOG.md)
- [Commits](https://github.com/fsnotify/fsnotify/compare/v1.6.0...v1.8.0)

---
updated-dependencies:
- dependency-name: github.com/fsnotify/fsnotify
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-17 11:24:53 +00:00
dependabot[bot]
490cfcafd0
chore(deps): bump golangci/golangci-lint-action from 6.3.3 to 6.5.0 (#697)
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 6.3.3 to 6.5.0.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v6.3.3...v6.5.0)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-17 11:13:28 +00:00
Andrey Nering
7fbc040cc8 refactor: rename s to content 2025-02-13 09:10:00 -08:00
shane.xb.qian
9dd8a6f3fd fix: make pipeline for tui work
* 'cat foo.md | glow -t'

Signed-off-by: shane.xb.qian <shane.qian@foxmail.com>
2025-02-13 09:10:00 -08:00
Charm
1b840ba227
ci: sync dependabot config (#695) 2025-02-13 11:17:14 -03:00
dependabot[bot]
436aa682be
chore(deps): bump github.com/charmbracelet/bubbles from 0.18.0 to 0.20.0 (#690)
Bumps [github.com/charmbracelet/bubbles](https://github.com/charmbracelet/bubbles) from 0.18.0 to 0.20.0.
- [Release notes](https://github.com/charmbracelet/bubbles/releases)
- [Changelog](https://github.com/charmbracelet/bubbles/blob/master/.goreleaser.yml)
- [Commits](https://github.com/charmbracelet/bubbles/compare/v0.18.0...v0.20.0)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/bubbles
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-13 13:51:18 +00:00
dependabot[bot]
305ea9d447
chore(deps): bump golang.org/x/term from 0.22.0 to 0.29.0 (#694)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.22.0 to 0.29.0.
- [Commits](https://github.com/golang/term/compare/v0.22.0...v0.29.0)

---
updated-dependencies:
- dependency-name: golang.org/x/term
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-13 13:49:30 +00:00
dependabot[bot]
929a9e326f
chore(deps): bump github.com/charmbracelet/x/editor (#692)
Bumps [github.com/charmbracelet/x/editor](https://github.com/charmbracelet/x) from 0.0.0-20240625164403-2627ec16405d to 0.1.0.
- [Release notes](https://github.com/charmbracelet/x/releases)
- [Commits](https://github.com/charmbracelet/x/commits/v0.1.0)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/x/editor
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-13 13:46:44 +00:00
dependabot[bot]
8850757e80
chore(deps): bump github.com/charmbracelet/bubbletea (#693)
Bumps [github.com/charmbracelet/bubbletea](https://github.com/charmbracelet/bubbletea) from 0.26.7-0.20240711210516-ea13ffb9a18d to 1.3.3.
- [Release notes](https://github.com/charmbracelet/bubbletea/releases)
- [Changelog](https://github.com/charmbracelet/bubbletea/blob/main/.goreleaser.yml)
- [Commits](https://github.com/charmbracelet/bubbletea/commits/v1.3.3)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/bubbletea
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-13 13:45:29 +00:00
dependabot[bot]
4db0304573
chore(deps): bump golangci/golangci-lint-action from 6.3.2 to 6.3.3 (#689)
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 6.3.2 to 6.3.3.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v6.3.2...v6.3.3)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-13 13:43:46 +00:00
dependabot[bot]
19c6af747f
chore(deps): bump golang.org/x/text from 0.16.0 to 0.22.0 (#691)
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.16.0 to 0.22.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.16.0...v0.22.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-13 13:43:38 +00:00
Charm
b17f04c0ef
ci: sync dependabot config (#688) 2025-02-13 10:40:53 -03:00
Shane-XB-Qian
546b6287d8
ci: upgrade go and golangci-lint ver (#685) 2025-02-12 11:11:44 -03:00
Shane-XB-Qian
f689c90084
fix: correct abs to rel path conversion (#683) 2025-02-11 10:53:34 -03:00
Andrey Nering
51fa19b8b4 ci: fix semgrep 2025-02-11 05:35:31 -08:00
Andrey Nering
05e5b2e279 feat: watch for file changes and reload automatically 2025-02-11 05:35:31 -08:00
Andrey Nering
e44e4684e1 fix: fallback to the current directory if no argument was given 2025-02-11 05:35:31 -08:00
Andrey Nering
2671509003 fix: make all setting false by default 2025-02-07 08:16:33 -08:00
Andrey Nering
8e363644f6 chore: add missing line end to .gitignore 2025-02-07 08:16:33 -08:00
Andrey Nering
e03817b2cb
feat: add new --tui / -t flag (#679) 2025-02-05 16:45:36 -03:00
Andrey Nering
009c37602a
fix: restore hint for e / edit on the footer (#678) 2025-02-03 13:20:54 -08:00
NijeboerFrank
3989fe0d5e
fix: ensure r key works when filtering files (#648) 2025-02-03 17:03:38 -03:00
bashbunni
7b3bfac306
test: skip networked tests if no connection (#661)
* test: skip networked tests if no connection

* fix: handle unexpected http responses

* fix: return if a valid URL is generated

* test: delete networked test
2024-12-18 11:10:03 -08:00
bashbunni
f255f3a603
feat: support GLAMOUR_STYLE for custom pager styles (#587)
* feat: support GLAMOUR_STYLE for custom pager styles

* fix: resolve errors
2024-12-16 07:55:09 -08:00
Carlos Alexandro Becker
ae57cced06
fix: remove more stash-related stuff (#656)
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-10-31 09:53:59 -03:00
Stanislav (Stas) Katkov
f1b565125f
feat: make --all configurable globally (#651)
* Make --all configurable globally

* docs: add all option to docs and default config

* refactor: tidy fields, set showAllFiles to true if unset

---------

Co-authored-by: Stanislav (Stas) Katkov <github@skatkov.com>
Co-authored-by: bashbunni <bunni@bashbunni.dev>
2024-10-01 10:23:20 -07:00
Christian Rocha
67243bb6fb
docs(license): update copyright date 2024-08-23 15:00:20 -04:00
Christian Rocha
f02c9a8fdc
docs(readme): update GIF 2024-08-23 14:49:31 -04:00
Christian Rocha
b26341ec7b docs(readme): pull network stuff from config example 2024-08-23 13:25:12 -04:00
Carlos Alexandro Becker
736e77dbc6
feat!: rename module to /v2 (#640)
Next version of Glow drops stash support, which is a breaking change.

This makes it accordingly.

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-08-23 12:45:30 -03:00
Carlos Alexandro Becker
cd99146e9e
fix: update codeowners (#637)
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-08-23 12:45:18 -03:00
Carlos Alexandro Becker
3f3b2c4586
test: github:// proto 2024-08-21 15:07:42 -03:00
Carlos Alexandro Becker
c89c8bd6e7
feat: update glamour (#634)
* feat: updating glamour

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

* fix: trim space

* chore: bump

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

* chore(deps): bump

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

* fix: perms

* fix: improve logs

* feat: update glamour

---------

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-08-21 14:43:05 -03:00
Carlos Alexandro Becker
4578bc149a
feat: updating glamour (#631)
* feat: updating glamour

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

* fix: trim space

* chore: bump

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

* chore(deps): bump

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

---------

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-07-25 10:32:08 -04:00
Carlos Alexandro Becker
0dcf1271dd
feat: suspend
closes #333
2024-07-11 18:07:35 -03:00
Carlos Alexandro Becker
94835d99ee
test: skip flukish test 2024-07-11 12:20:59 -03:00
kalle (jag)
96d2a38e67
feat: allow --width=0 to disable wordwrap (#366)
Co-authored-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-07-11 12:16:12 -03:00
Carlos Alexandro Becker
2d5bd3b799
feat: show line numbers configuration option
closes #311
2024-07-11 12:08:57 -03:00
Carlos Alexandro Becker
247c707b25
fix: glow URL
closes #464
2024-07-11 12:02:03 -03:00
Carlos Alexandro Becker
c356273a76
chore: update glamour to master
fixes  #485

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-07-10 17:02:37 -03:00
Carlos Alexandro Becker
3f9102d116
fix: yet another log handling fix 2024-07-10 12:02:50 -03:00
Carlos Alexandro Becker
a1a85636ea
fix: handle not being able to create log file 2024-07-10 10:42:21 -03:00
Carlos Alexandro Becker
b80e8312f2
fix: handle permission denied 2024-07-10 10:39:05 -03:00
Carlos Alexandro Becker
39c336f823
ci: improve coverage workflow 2024-07-10 10:37:00 -03:00
Carlos Alexandro Becker
e4cacf7319
fix: mkdir cache dir 2024-07-10 10:35:52 -03:00
Carlos Alexandro Becker
ad01f380bd
fix: improve line no 2024-07-09 16:46:49 -03:00
Carlos Alexandro Becker
597e56bad4
feat: open editor in current line
closes #547
2024-07-09 16:38:32 -03:00
Carlos Alexandro Becker
400228cff6
docs: update install 2024-07-09 16:38:10 -03:00
Carlos Alexandro Becker
1407793c5d
fix: pass color profile down to glamour (#626)
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-07-09 16:13:32 -03:00
Carlos Alexandro Becker
401610afa4
ci: run govulncheck, semgrep, etc (#627)
* ci: run govulncheck, semgrep, etc

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

* fix: error check

---------

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-07-09 16:13:05 -03:00
Carlos Alexandro Becker
9ebe39cd09
feat: improve gitlab/github readme url (#456)
* Use GitHub API to find readme filename

* Fix lint errors and typos

* Bring back "tries to find" instead of "finds"

* Rename `readmeURL` to `apiURL`

* Don't close body

* Use GitLab API to find readme filename

* feat: improve gitlab/github readme url

Signed-off-by: Carlos A Becker <caarlos0@users.noreply.github.com>

---------

Signed-off-by: Carlos A Becker <caarlos0@users.noreply.github.com>
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
Co-authored-by: danielwerg <35052399+danielwerg@users.noreply.github.com>
2024-07-09 16:07:39 -03:00
Carlos Alexandro Becker
ab94744c39
chore: fix codeowners
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-07-09 16:04:11 -03:00
Carlos Alexandro Becker
2288311e82
fix: do not hard fail if no config
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-07-09 10:58:52 -03:00
Carlos Alexandro Becker
dbf6e3e799
ci: fix goreleaser config
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-07-09 10:48:54 -03:00
Carlos Alexandro Becker
1133e5047b
fix: config failing in tests (#625)
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-07-09 10:47:44 -03:00
Carlos Alexandro Becker
f44e5b5f5c
fix: improve message
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-07-09 10:17:18 -03:00
Carlos Alexandro Becker
d89d79a00c
feat: --preserve-new-lines (#623)
closes #502
2024-07-09 09:50:10 -03:00
Carlos Alexandro Becker
fe066f2140
feat: allow config from CHARM_CONFIG_HOME and XDG_CONFIG_HOME (#621)
* feat: allow config from CHARM_CONFIG_HOME

closes #594
closes #495
closes #328
closes #475

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

* fix: improvements on config handling

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

* chore: log

---------

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-07-08 22:33:49 -03:00
Carlos Alexandro Becker
b1d377237d
feat: add 'r' to refresh list and document (#624)
* feat: add 'r' to refresh list

closes #416

Co-authored-by: Dieter Eickstaedt <eickstaedt@deicon.de>

* feat: refresh document

Closes #501

Co-authored-by: fedeztk <federicoserranexus@gmail.com>

---------

Co-authored-by: Dieter Eickstaedt <eickstaedt@deicon.de>
Co-authored-by: fedeztk <federicoserranexus@gmail.com>
2024-07-08 16:15:08 -03:00
haennes
0a7db86725
docs: use nix-shell as nix-env -iA is bad practice (#622) 2024-07-08 11:19:02 -03:00
Carlos Alexandro Becker
f1cf0ba785
feat: man pages
closes #269
2024-07-03 13:59:11 -03:00
Carlos Alexandro Becker
d7032fe57f
feat: shell completions
closes #533
closes #546
closes #512
2024-07-03 13:41:11 -03:00
Carlos Alexandro Becker
821d51da13
fix: do not show absolute path as note 2024-07-03 13:36:17 -03:00
Carlos Alexandro Becker
dbfcc27dad
chore: go mod tidy
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-07-03 13:27:07 -03:00
Carlos Alexandro Becker
380f71086c
Merge remote-tracking branch 'origin/master' 2024-07-03 13:26:53 -03:00
Aitva
08c564c858
fix: --all bypass .gitignore rules (#285) (#504)
* feat(deps): bump github.com/muesli/gitcha from 0.2.0 to 0.3.0

* fix: --all bypass .gitignore rules (#285)

* Update ui/ui.go

---------

Co-authored-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-07-03 13:19:19 -03:00
Carlos Alexandro Becker
e31dc2c890
Merge remote-tracking branch 'origin/master' 2024-07-03 13:16:12 -03:00
Carlos Alexandro Becker
8e51396575
fix: check other possible readme paths/branches (#450)
* fix: check other possible readme paths/branches

Signed-off-by: Carlos A Becker <caarlos0@users.noreply.github.com>

* fix: url

Signed-off-by: Carlos A Becker <caarlos0@users.noreply.github.com>

* fix: Readme.md

---------

Signed-off-by: Carlos A Becker <caarlos0@users.noreply.github.com>
2024-07-03 13:09:20 -03:00
rustfix
f3d0cb89da
chore: fix typo in comment (#607)
Signed-off-by: rustfix <771054535@qq.com>
2024-07-03 13:06:57 -03:00
Carlos Alexandro Becker
7e471f1787
fix: lint issue
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-07-03 13:05:46 -03:00
dependabot[bot]
3ee20895b1
chore(deps): bump github.com/charmbracelet/glamour from 0.6.0 to 0.7.0 (#600)
Bumps [github.com/charmbracelet/glamour](https://github.com/charmbracelet/glamour) from 0.6.0 to 0.7.0.
- [Release notes](https://github.com/charmbracelet/glamour/releases)
- [Commits](https://github.com/charmbracelet/glamour/compare/v0.6.0...v0.7.0)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/glamour
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-03 12:26:38 -03:00
dependabot[bot]
c47e2acd30
chore(deps): bump golangci/golangci-lint-action from 4 to 6 (#612)
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 4 to 6.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v4...v6)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-03 12:26:01 -03:00
Carlos Alexandro Becker
fce3edf7db
feat!: cleanup and updated (#619)
* feat!: cleanup

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

* more cleanup

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

* fix: more cleanup

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

* fix: more cleanup

---------

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-07-03 12:11:29 -03:00
Ayman Bagabas
d2e7742a6a
chore(ci): enable gofumpt 2024-05-22 11:48:31 -04:00
Maas Lalani
2430b0af3f Create CODEOWNERS 2024-02-28 21:01:19 -05:00
dependabot[bot]
cece1377cd
chore(deps): bump github.com/mattn/go-runewidth from 0.0.14 to 0.0.15 (#576)
Bumps [github.com/mattn/go-runewidth](https://github.com/mattn/go-runewidth) from 0.0.14 to 0.0.15.
- [Commits](https://github.com/mattn/go-runewidth/compare/v0.0.14...v0.0.15)

---
updated-dependencies:
- dependency-name: github.com/mattn/go-runewidth
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-13 12:59:38 -05:00
dependabot[bot]
feefb88e70
chore(deps): bump github.com/spf13/cobra from 1.6.1 to 1.8.0 (#577)
Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 1.6.1 to 1.8.0.
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/v1.6.1...v1.8.0)

---
updated-dependencies:
- dependency-name: github.com/spf13/cobra
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-13 12:59:24 -05:00
dependabot[bot]
ec12c2717d
feat(deps): bump golang.org/x/sys from 0.15.0 to 0.17.0 (#570)
Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.15.0 to 0.17.0.
- [Commits](https://github.com/golang/sys/compare/v0.15.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/sys
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-13 12:59:13 -05:00
dependabot[bot]
f1437fce69
chore(deps): bump github.com/dustin/go-humanize (#575)
Bumps [github.com/dustin/go-humanize](https://github.com/dustin/go-humanize) from 1.0.1-0.20200219035652-afde56e7acac to 1.0.1.
- [Commits](https://github.com/dustin/go-humanize/commits/v1.0.1)

---
updated-dependencies:
- dependency-name: github.com/dustin/go-humanize
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-13 12:54:52 -05:00
dependabot[bot]
79c39932fb
chore(deps): bump golangci/golangci-lint-action from 3 to 4 (#572)
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 3 to 4.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-13 12:54:32 -05:00
dependabot[bot]
64cc6a6baa
chore(deps): bump actions/checkout from 3 to 4 (#574)
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-13 12:54:24 -05:00
dependabot[bot]
8221279fa3
chore(deps): bump actions/setup-go from 4 to 5 (#573)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 4 to 5.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-13 12:54:12 -05:00
Ayman Bagabas
dd035fb441
chore(ci): change dependabot pr's title 2024-02-13 12:43:41 -05:00
dependabot[bot]
d9bb637f5e
feat(deps): bump github.com/charmbracelet/bubbletea (#567) 2024-02-13 12:37:42 -05:00
dependabot[bot]
6d86484b5e
feat(deps): bump golang.org/x/net from 0.12.0 to 0.17.0 (#568)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.12.0 to 0.17.0.
- [Commits](https://github.com/golang/net/compare/v0.12.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-13 12:08:10 -05:00
dependabot[bot]
2b91a93645
feat(deps): bump golang.org/x/crypto from 0.11.0 to 0.17.0 (#569)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.11.0 to 0.17.0.
- [Commits](https://github.com/golang/crypto/compare/v0.11.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-13 12:00:18 -05:00
Ayman Bagabas
bccf3d0cb7
fix(ci): update coverage workflow 2024-02-13 11:51:30 -05:00
K.B.Dharun Krishna
1eacfb4ff1
docs: add Winget to Windows Install Options (#500)
* README: add Winget to Windows Install Options

* Update README.md

Co-authored-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

---------

Co-authored-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2023-12-07 09:19:20 -03:00
Carlos Alexandro Becker
c991ec475d
feat: use x/editor (#543)
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2023-11-17 09:02:52 -03:00
Carlos Alexandro Becker
54dd62a2a4
fix: handle running inside a snap (#527)
* fix: handle running inside a snap

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

* test: fixes

* docs: improve error message

---------

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
Co-authored-by: bashbunni <bunni@bashbunni.dev>
2023-08-28 09:05:12 -03:00
Lars Müller
9a3f7a19cb
fix: replace string concatenation in loop with string builder (#505) 2023-08-24 13:52:15 -03:00
Carlos Alexandro Becker
f0734709f0 feat(deps): update some dependencies
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2023-07-13 14:47:06 +02:00
bashbunni
58ec351692
docs: add issue template (#486)
* docs: add issue template

* chore(template): add locale
2023-05-23 11:57:58 -07:00
Ayman Bagabas
9c79fa1b48
fix(ci): remove soft-serve workflow 2023-05-12 14:08:04 -04:00
Carlos Alexandro Becker
8aed0d9092
docs: update license
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2023-05-11 12:38:59 +00:00
Christian Muehlhaeuser
ad21129703 chore: bump charm dep 2023-05-09 11:16:02 +02:00
Christian Muehlhaeuser
8c80ea5f67 fix: lazily init UI 2023-05-09 11:16:02 +02:00
dependabot[bot]
4a3d98697b chore(deps): bump actions/setup-go from 3 to 4
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 3 to 4.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-16 11:04:01 +01:00
Kian-Meng Ang
442bc281c3 docs: fix typos
Found via `typos --format brief`
2023-03-02 22:22:16 -05:00
51 changed files with 1594 additions and 3694 deletions

18
.editorconfig Normal file
View file

@ -0,0 +1,18 @@
# https://editorconfig.org/
root = true
[*]
charset = utf-8
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 2
[*.go]
indent_style = tab
indent_size = 8
[*.golden]
insert_final_newline = false
trim_trailing_whitespace = false

38
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View file

@ -0,0 +1,38 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**Setup**
Please complete the following information along with version numbers, if applicable.
- OS [e.g. Ubuntu, macOS]
- Shell [e.g. zsh, fish]
- Terminal Emulator [e.g. kitty, iterm]
- Terminal Multiplexer [e.g. tmux]
- Locale [e.g. en_US.UTF-8, zh_CN.UTF-8, etc.]
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Source Code**
Please include source code if needed to reproduce the behavior.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
Add screenshots to help explain your problem.
**Additional context**
Add any other context about the problem here.

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View file

@ -0,0 +1,5 @@
blank_issues_enabled: true
contact_links:
- name: Discord
url: https://charm.sh/discord
about: Chat on our Discord.

View file

@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View file

@ -1,20 +1,57 @@
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "daily"
labels:
- "dependencies"
commit-message:
prefix: "feat"
include: "scope"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
interval: "weekly"
day: "monday"
time: "05:00"
timezone: "America/New_York"
labels:
- "dependencies"
commit-message:
prefix: "chore"
include: "scope"
groups:
all:
patterns:
- "*"
ignore:
- dependency-name: github.com/charmbracelet/bubbletea/v2
versions:
- v2.0.0-beta1
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "05:00"
timezone: "America/New_York"
labels:
- "dependencies"
commit-message:
prefix: "chore"
include: "scope"
groups:
all:
patterns:
- "*"
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "05:00"
timezone: "America/New_York"
labels:
- "dependencies"
commit-message:
prefix: "chore"
include: "scope"
groups:
all:
patterns:
- "*"

View file

@ -10,3 +10,16 @@ jobs:
uses: charmbracelet/meta/.github/workflows/snapshot.yml@main
secrets:
goreleaser_key: ${{ secrets.GORELEASER_KEY }}
govulncheck:
uses: charmbracelet/meta/.github/workflows/govulncheck.yml@main
with:
go-version: stable
semgrep:
uses: charmbracelet/meta/.github/workflows/semgrep.yml@main
ruleguard:
uses: charmbracelet/meta/.github/workflows/ruleguard.yml@main
with:
go-version: stable

View file

@ -3,26 +3,22 @@ on: [push, pull_request]
jobs:
coverage:
strategy:
matrix:
go-version: [^1]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
runs-on: ubuntu-latest
env:
GO111MODULE: "on"
steps:
- name: Install Go
uses: actions/setup-go@v3
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v6
- name: Install Go
uses: actions/setup-go@v6
with:
go-version: stable
- name: Coverage
env:
COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
go test -race -covermode atomic -coverprofile=profile.cov ./...
GO111MODULE=off go get github.com/mattn/goveralls
$(go env GOPATH)/bin/goveralls -coverprofile=profile.cov -service=github
go install github.com/mattn/goveralls@latest
goveralls -coverprofile=profile.cov -service=github

17
.github/workflows/dependabot-sync.yml vendored Normal file
View file

@ -0,0 +1,17 @@
name: dependabot-sync
on:
schedule:
- cron: "0 0 * * 0" # every Sunday at midnight
workflow_dispatch: # allows manual triggering
permissions:
contents: write
pull-requests: write
jobs:
dependabot-sync:
uses: charmbracelet/meta/.github/workflows/dependabot-sync.yml@main
with:
repo_name: ${{ github.event.repository.name }}
secrets:
gh_token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}

View file

@ -1,3 +1,5 @@
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
name: goreleaser
on:
@ -21,5 +23,3 @@ jobs:
nfpm_gpg_key: ${{ secrets.NFPM_GPG_KEY }}
nfpm_passphrase: ${{ secrets.NFPM_PASSPHRASE }}
snapcraft_token: ${{ secrets.SNAPCRAFT_TOKEN }}
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json

13
.github/workflows/lint-sync.yml vendored Normal file
View file

@ -0,0 +1,13 @@
name: lint-sync
on:
schedule:
- cron: "0 0 * * 0" # every sunday at midnight
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
lint:
uses: charmbracelet/meta/.github/workflows/lint-sync.yml@main

View file

@ -8,9 +8,9 @@ jobs:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v6
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
uses: golangci/golangci-lint-action@v9.2.0
with:
# Optional: golangci-lint command line arguments.
args: --issues-exit-code=0

View file

@ -1,12 +0,0 @@
name: soft-serve
on:
push:
branches:
- master
jobs:
soft-serve:
uses: charmbracelet/meta/.github/workflows/soft-serve.yml@main
secrets:
ssh-key: "${{ secrets.CHARM_SOFT_SERVE_KEY }}"

2
.gitignore vendored
View file

@ -2,4 +2,4 @@ glow
dist/
.envrc
completions/
manpages/
manpages/

View file

@ -1,34 +1,44 @@
version: "2"
run:
tests: false
issues:
include:
- EXC0001
- EXC0005
- EXC0011
- EXC0012
- EXC0013
max-issues-per-linter: 0
max-same-issues: 0
linters:
enable:
- bodyclose
- dupl
- exportloopref
- exhaustive
- goconst
- godot
- godox
- goimports
- gomoddirectives
- goprintffuncname
- gosec
- ifshort
- misspell
- nakedret
- nestif
- nilerr
- noctx
- nolintlint
- prealloc
- revive
- rowserrcheck
- sqlclosecheck
- tparallel
- unconvert
- unparam
- whitespace
- wrapcheck
exclusions:
rules:
- text: '(slog|log)\.\w+'
linters:
- noctx
generated: lax
presets:
- common-false-positives
issues:
max-issues-per-linter: 0
max-same-issues: 0
formatters:
enable:
- gofumpt
- goimports
exclusions:
generated: lax

View file

@ -1,14 +1,14 @@
# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json
version: 2
includes:
- from_url:
url: charmbracelet/meta/main/goreleaser-glow.yaml
variables:
main: "."
binary_name: glow
description: "Render markdown on the CLI, with pizzazz!"
github_url: "https://github.com/charmbracelet/soft-serve"
github_url: "https://github.com/charmbracelet/glow"
maintainer: "Christian Muehlhaeuser <muesli@charm.sh>"
brew_commit_author_name: "Christian Muehlhaeuser"
brew_commit_author_email: "muesli@charm.sh"
# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2019-2022 Charmbracelet, Inc
Copyright (c) 2019-2024 Charmbracelet, Inc
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -1,20 +0,0 @@
# This Makefile is just for development purposes
.PHONY: default clean glow run log
LOGFILE := debug.log
default: glow
clean:
rm -f ./glow
glow:
go build
run: clean glow
GLOW_LOGFILE=$(LOGFILE) ./glow
log:
> $(LOGFILE)
tail -f $(LOGFILE)

104
README.md
View file

@ -11,7 +11,7 @@ Render markdown on the CLI, with _pizzazz_!
</p>
<p align="center">
<img src="https://stuff.charm.sh/glow/glow-1.3-trailer-github.gif" width="600" alt="Glow UI Demo">
<img src="https://github.com/user-attachments/assets/c2246366-f84b-4847-b431-32a61ca07b74" width="800" alt="Glow UI Demo">
</p>
## What is it?
@ -20,13 +20,9 @@ Glow is a terminal based markdown reader designed from the ground up to bring
out the beauty—and power—of the CLI.
Use it to discover markdown files, read documentation directly on the command
line and stash markdown files to your own private collection, so you can read
them anywhere. Glow will find local markdown files in subdirectories or a local
line. Glow will find local markdown files in subdirectories or a local
Git repository.
By the way, all data stashed is encrypted end-to-end: only you can decrypt it.
More on that below.
## Installation
### Package Manager
@ -34,38 +30,64 @@ More on that below.
```bash
# macOS or Linux
brew install glow
```
```bash
# macOS (with MacPorts)
sudo port install glow
```
```bash
# Arch Linux (btw)
pacman -S glow
```
```bash
# Void Linux
xbps-install -S glow
```
# Nix
nix-env -iA nixpkgs.glow
```bash
# Nix shell
nix-shell -p glow --command glow
```
```bash
# FreeBSD
pkg install glow
```
```bash
# Solus
eopkg install glow
```
# Windows (with Scoop or Chocolatey)
scoop install glow
```bash
# Windows (with Chocolatey, Scoop, or Winget)
choco install glow
scoop install glow
winget install charmbracelet.glow
```
```bash
# Android (with termux)
pkg install glow
```
```bash
# Ubuntu (Snapcraft)
sudo snap install glow
```
```bash
# Debian/Ubuntu
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://repo.charm.sh/apt/gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/charm.gpg
echo "deb [signed-by=/etc/apt/keyrings/charm.gpg] https://repo.charm.sh/apt/ * *" | sudo tee /etc/apt/sources.list.d/charm.list
sudo apt update && sudo apt install glow
```
```bash
# Fedora/RHEL
echo '[charm]
name=Charm
@ -83,11 +105,12 @@ packages. ARM builds are also available for macOS, Linux, FreeBSD and OpenBSD.
### Go
Or just install it with `go`:
```bash
go install github.com/charmbracelet/glow@latest
go install github.com/charmbracelet/glow/v2@latest
```
### Build (requires Go 1.13+)
### Build (requires Go 1.21+)
```bash
git clone https://github.com/charmbracelet/glow.git
@ -97,11 +120,10 @@ go build
[releases]: https://github.com/charmbracelet/glow/releases
## The TUI
Simply run `glow` without arguments to start the textual user interface and
browse local and stashed markdown. Glow will find local markdown files in the
browse local. Glow will find local markdown files in the
current directory and below or, if youre in a Git repository, Glow will search
the repo.
@ -109,15 +131,6 @@ Markdown files can be read with Glow's high-performance pager. Most of the
keystrokes you know from `less` are the same, but you can press `?` to list
the hotkeys.
### Stashing
Glow works with the Charm Cloud to allow you to store any markdown files in
your own private collection. You can stash a local document from the Glow TUI by
pressing `s`.
Stashing is private, its contents will not be exposed publicly, and it's
encrypted end-to-end. More on encryption below.
## The CLI
In addition to a TUI, Glow has a CLI for working with Markdown. To format a
@ -137,18 +150,6 @@ glow github.com/charmbracelet/glow
glow https://host.tld/file.md
```
### Stashing
You can also stash documents from the CLI:
```bash
glow stash README.md
```
Then, when you run `glow` without arguments will you can browse through your
stashed documents. This is a great way to keep track of things that you need to
reference often.
### Word Wrapping
The `-w` flag lets you set a maximum width at which the output will be wrapped:
@ -200,42 +201,39 @@ Here's an example config:
```yaml
# style name or JSON path (default "auto")
style: "light"
# show local files only; no network (TUI-mode only)
local: true
# mouse support (TUI-mode only)
# mouse wheel support (TUI-mode only)
mouse: true
# use pager to display markdown
pager: true
# word-wrap at width
# at which column should we word wrap?
width: 80
# show all files, including hidden and ignored.
all: false
# show line numbers (TUI-mode only)
showLineNumbers: false
# preserve newlines in the output
preserveNewLines: false
```
## 🔒 Encryption: How It Works
## Contributing
Encryption works by issuing symmetric keys (basically a generated password) and
encrypting it with the local SSH public key generated by the open-source
[charm][charmlib] library. That encrypted key is then sent up to our server.
We cant read it since we dont have your private key. When you want to decrypt
something or view your stash, that key is downloaded from our server and
decrypted locally using the SSH private key. When you link accounts, the
symmetric key is encrypted for each new public key. This happens on your
machine and not our server, so we never see any unencrypted data.
See [contributing][contribute].
[charmlib]: https://github.com/charmbracelet/charm
[contribute]: https://github.com/charmbracelet/glow/contribute
## Feedback
Wed love to hear your thoughts on this project. Feel free to drop us a note!
* [Twitter](https://twitter.com/charmcli)
* [The Fediverse](https://mastodon.social/@charmcli)
* [Discord](https://charm.sh/chat)
- [Twitter](https://twitter.com/charmcli)
- [The Fediverse](https://mastodon.social/@charmcli)
- [Discord](https://charm.sh/chat)
## License
[MIT](https://github.com/charmbracelet/glow/raw/master/LICENSE)
***
---
Part of [Charm](https://charm.sh).

23
Taskfile.yaml Normal file
View file

@ -0,0 +1,23 @@
# https://taskfile.dev
version: '3'
tasks:
lint:
desc: Run base linters
cmds:
- golangci-lint run
test:
desc: Run tests
cmds:
- go test ./... {{.CLI_ARGS}}
log:
desc: Watch for glow logs
aliases: [tail]
cmds:
- cmd: tail -f ~/Library/Caches/glow/glow.log
platforms: [darwin]
- cmd: tail -f ~/.cache/glow/glow.log
platforms: [linux, windows]

View file

@ -1,26 +1,29 @@
package main
import (
"errors"
"fmt"
"io/fs"
"os"
"path"
"path/filepath"
"github.com/charmbracelet/glow/editor"
gap "github.com/muesli/go-app-paths"
"github.com/charmbracelet/x/editor"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
const defaultConfig = `# style name or JSON path (default "auto")
style: "auto"
# show local files only; no network (TUI-mode only)
local: false
# mouse support (TUI-mode only)
mouse: false
# use pager to display markdown
pager: false
# word-wrap at width
width: 80`
width: 80
# show all files, including hidden and ignored.
all: false
`
var configCmd = &cobra.Command{
Use: "config",
@ -29,50 +32,57 @@ var configCmd = &cobra.Command{
Long: paragraph(fmt.Sprintf("\n%s the glow config file. Well use EDITOR to determine which editor to use. If the config file doesn't exist, it will be created.", keyword("Edit"))),
Example: paragraph("glow config\nglow config --config path/to/config.yml"),
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
if configFile == "" {
scope := gap.NewScope(gap.User, "glow")
var err error
configFile, err = scope.ConfigPath("glow.yml")
if err != nil {
return err
}
}
if ext := path.Ext(configFile); ext != ".yaml" && ext != ".yml" {
return fmt.Errorf("'%s' is not a supported config type: use '%s' or '%s'", ext, ".yaml", ".yml")
}
if _, err := os.Stat(configFile); os.IsNotExist(err) {
// File doesn't exist yet, create all necessary directories and
// write the default config file
if err := os.MkdirAll(filepath.Dir(configFile), 0o700); err != nil {
return err
}
f, err := os.Create(configFile)
if err != nil {
return err
}
defer func() { _ = f.Close() }()
if _, err := f.WriteString(defaultConfig); err != nil {
return err
}
} else if err != nil { // some other error occurred
RunE: func(*cobra.Command, []string) error {
if err := ensureConfigFile(); err != nil {
return err
}
c := editor.Cmd(configFile)
c, err := editor.Cmd("Glow", configFile)
if err != nil {
return fmt.Errorf("unable to set config file: %w", err)
}
c.Stdin = os.Stdin
c.Stdout = os.Stdout
c.Stderr = os.Stderr
if err := c.Run(); err != nil {
return err
return fmt.Errorf("unable to run command: %w", err)
}
fmt.Println("Wrote config file to:", configFile)
return nil
},
}
func ensureConfigFile() error {
if configFile == "" {
configFile = viper.GetViper().ConfigFileUsed()
if err := os.MkdirAll(filepath.Dir(configFile), 0o755); err != nil { //nolint:gosec
return fmt.Errorf("could not write configuration file: %w", err)
}
}
if ext := path.Ext(configFile); ext != ".yaml" && ext != ".yml" {
return fmt.Errorf("'%s' is not a supported configuration type: use '%s' or '%s'", ext, ".yaml", ".yml")
}
if _, err := os.Stat(configFile); errors.Is(err, fs.ErrNotExist) {
// File doesn't exist yet, create all necessary directories and
// write the default config file
if err := os.MkdirAll(filepath.Dir(configFile), 0o700); err != nil {
return fmt.Errorf("unable create directory: %w", err)
}
f, err := os.Create(configFile)
if err != nil {
return fmt.Errorf("unable to create config file: %w", err)
}
defer func() { _ = f.Close() }()
if _, err := f.WriteString(defaultConfig); err != nil {
return fmt.Errorf("unable to write config file: %w", err)
}
} else if err != nil { // some other error occurred
return fmt.Errorf("unable to stat config file: %w", err)
}
return nil
}

View file

@ -1,27 +0,0 @@
package editor
import (
"os"
"os/exec"
"strings"
)
const defaultEditor = "nano"
// Cmd returns a *exec.Cmd editing the given path with $EDITOR or nano if no
// $EDITOR is set.
func Cmd(path string) *exec.Cmd {
editor, args := getEditor()
return exec.Command(editor, append(args, path)...)
}
func getEditor() (string, []string) {
editor := strings.Fields(os.Getenv("EDITOR"))
if len(editor) > 1 {
return editor[0], editor[1:]
}
if len(editor) == 1 {
return editor[0], []string{}
}
return defaultEditor, []string{}
}

View file

@ -1,26 +0,0 @@
package editor
import (
"reflect"
"testing"
)
func TestEditor(t *testing.T) {
filename := "README.md"
for k, v := range map[string][]string{
"": {"nano", filename},
"nvim": {"nvim", filename},
"vim": {"vim", filename},
"vscode --foo": {"vscode", "--foo", filename},
"nvim -a -b": {"nvim", "-a", "-b", filename},
} {
t.Run(k, func(t *testing.T) {
t.Setenv("EDITOR", k)
cmd := Cmd("README.md")
got := cmd.Args
if !reflect.DeepEqual(got, v) {
t.Fatalf("expected %v; got %v", v, got)
}
})
}
}

View file

@ -1,47 +1,55 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
)
// isGitHubURL tests a string to determine if it is a well-structured GitHub URL.
func isGitHubURL(s string) (string, bool) {
if strings.HasPrefix(s, "github.com/") {
s = "https://" + s
// findGitHubREADME tries to find the correct README filename in a repository using GitHub API.
func findGitHubREADME(u *url.URL) (*source, error) {
owner, repo, ok := strings.Cut(strings.TrimPrefix(u.Path, "/"), "/")
if !ok {
return nil, fmt.Errorf("invalid url: %s", u.String())
}
u, err := url.ParseRequestURI(s)
type readme struct {
DownloadURL string `json:"download_url"`
}
apiURL := fmt.Sprintf("https://api.%s/repos/%s/%s/readme", u.Hostname(), owner, repo)
//nolint:bodyclose
// it is closed on the caller
res, err := http.Get(apiURL) //nolint: gosec,noctx
if err != nil {
return "", false
return nil, fmt.Errorf("unable to get url: %w", err)
}
return u.String(), strings.ToLower(u.Host) == "github.com"
}
// findGitHubREADME tries to find the correct README filename in a repository.
func findGitHubREADME(s string) (*source, error) {
u, err := url.ParseRequestURI(s)
body, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
return nil, fmt.Errorf("unable to read http response body: %w", err)
}
u.Host = "raw.githubusercontent.com"
for _, r := range readmeNames {
v := u
v.Path += "/master/" + r
var result readme
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("unable to parse json: %w", err)
}
// nolint:bodyclose
if res.StatusCode == http.StatusOK {
//nolint:bodyclose
// it is closed on the caller
resp, err := http.Get(v.String())
resp, err := http.Get(result.DownloadURL) //nolint: noctx
if err != nil {
return nil, err
return nil, fmt.Errorf("unable to get url: %w", err)
}
if resp.StatusCode == http.StatusOK {
return &source{resp.Body, v.String()}, nil
return &source{resp.Body, result.DownloadURL}, nil
}
}

View file

@ -1,46 +1,59 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
)
// isGitLabURL tests a string to determine if it is a well-structured GitLab URL.
func isGitLabURL(s string) (string, bool) {
if strings.HasPrefix(s, "gitlab.com/") {
s = "https://" + s
// findGitLabREADME tries to find the correct README filename in a repository using GitLab API.
func findGitLabREADME(u *url.URL) (*source, error) {
owner, repo, ok := strings.Cut(strings.TrimPrefix(u.Path, "/"), "/")
if !ok {
return nil, fmt.Errorf("invalid url: %s", u.String())
}
u, err := url.ParseRequestURI(s)
projectPath := url.QueryEscape(owner + "/" + repo)
type readme struct {
ReadmeURL string `json:"readme_url"`
}
apiURL := fmt.Sprintf("https://%s/api/v4/projects/%s", u.Hostname(), projectPath)
//nolint:bodyclose
// it is closed on the caller
res, err := http.Get(apiURL) //nolint: gosec,noctx
if err != nil {
return "", false
return nil, fmt.Errorf("unable to get url: %w", err)
}
return u.String(), strings.ToLower(u.Host) == "gitlab.com"
}
// findGitLabREADME tries to find the correct README filename in a repository.
func findGitLabREADME(s string) (*source, error) {
u, err := url.ParseRequestURI(s)
body, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
return nil, fmt.Errorf("unable to read http response body: %w", err)
}
for _, r := range readmeNames {
v := u
v.Path += "/raw/master/" + r
var result readme
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("unable to parse json: %w", err)
}
// nolint:bodyclose
readmeRawURL := strings.ReplaceAll(result.ReadmeURL, "blob", "raw")
if res.StatusCode == http.StatusOK {
//nolint:bodyclose
// it is closed on the caller
resp, err := http.Get(v.String())
resp, err := http.Get(readmeRawURL) //nolint: gosec,noctx
if err != nil {
return nil, err
return nil, fmt.Errorf("unable to get url: %w", err)
}
if resp.StatusCode == http.StatusOK {
return &source{resp.Body, v.String()}, nil
return &source{resp.Body, readmeRawURL}, nil
}
}

View file

@ -1,31 +1,9 @@
package main
import (
"bytes"
"testing"
)
func TestGlowSources(t *testing.T) {
tt := []string{
".",
"README.md",
"github.com/charmbracelet/glow",
"https://github.com/charmbracelet/glow",
}
for _, v := range tt {
buf := &bytes.Buffer{}
err := executeArg(rootCmd, v, buf)
if err != nil {
t.Errorf("Error during execution (args: %s): %v", v, err)
}
if buf.Len() == 0 {
t.Errorf("Output buffer should not be empty (args: %s)", v)
}
}
}
func TestGlowFlags(t *testing.T) {
tt := []struct {
args []string

86
go.mod
View file

@ -1,27 +1,75 @@
module github.com/charmbracelet/glow
module github.com/charmbracelet/glow/v2
go 1.16
go 1.25.12
toolchain go1.26.5
require (
github.com/atotto/clipboard v0.1.4
github.com/charmbracelet/bubbles v0.15.0
github.com/charmbracelet/bubbletea v0.23.1
github.com/charmbracelet/charm v0.8.6
github.com/charmbracelet/glamour v0.6.0
github.com/charmbracelet/lipgloss v0.6.0
github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac
github.com/mattn/go-runewidth v0.0.14
github.com/meowgorithm/babyenv v1.3.1
github.com/caarlos0/env/v11 v11.3.1
github.com/charmbracelet/bubbles v0.21.0
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/glamour v0.10.0
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834
github.com/charmbracelet/log v0.4.2
github.com/charmbracelet/x/editor v0.1.0
github.com/dustin/go-humanize v1.0.1
github.com/fsnotify/fsnotify v1.9.0
github.com/mattn/go-runewidth v0.0.19
github.com/mitchellh/go-homedir v1.1.0
github.com/muesli/gitcha v0.2.0
github.com/muesli/gitcha v0.3.0
github.com/muesli/go-app-paths v0.2.2
github.com/muesli/mango-cobra v1.3.0
github.com/muesli/reflow v0.3.0
github.com/muesli/termenv v0.13.0
github.com/sahilm/fuzzy v0.1.0
github.com/segmentio/ksuid v1.0.4
github.com/spf13/cobra v1.6.1
github.com/spf13/viper v1.14.0
golang.org/x/sys v0.0.0-20220908164124-27713097b956
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
golang.org/x/text v0.6.0
github.com/muesli/roff v0.1.0
github.com/muesli/termenv v0.16.0
github.com/sahilm/fuzzy v0.1.1
github.com/spf13/cobra v1.10.2
github.com/spf13/viper v1.21.0
golang.org/x/sys v0.43.0
golang.org/x/term v0.42.0
golang.org/x/text v0.39.0
mvdan.cc/sh/v3 v3.13.1
)
require (
github.com/alecthomas/chroma/v2 v2.14.0 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/x/ansi v0.10.1 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
github.com/dlclark/regexp2 v1.11.0 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/mango v0.2.0 // indirect
github.com/muesli/mango-pflag v0.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sabhiram/go-gitignore v0.0.0-20180611051255-d3107576ba94 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yuin/goldmark v1.7.17 // indirect
github.com/yuin/goldmark-emoji v1.0.5 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 // indirect
golang.org/x/net v0.53.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
)

1351
go.sum

File diff suppressed because it is too large Load diff

40
log.go Normal file
View file

@ -0,0 +1,40 @@
package main
import (
"fmt"
"io"
"os"
"path/filepath"
"github.com/charmbracelet/log"
gap "github.com/muesli/go-app-paths"
)
func getLogFilePath() (string, error) {
dir, err := gap.NewScope(gap.User, "glow").CacheDir()
if err != nil {
return "", fmt.Errorf("unable to get cache dir: %w", err)
}
return filepath.Join(dir, "glow.log"), nil
}
func setupLog() (func() error, error) {
log.SetOutput(io.Discard)
// Log to file, if set
logFile, err := getLogFilePath()
if err != nil {
return nil, err
}
if err := os.MkdirAll(filepath.Dir(logFile), 0o755); err != nil { //nolint:gosec
// log disabled
return func() error { return nil }, nil //nolint:nilerr
}
f, err := os.OpenFile(logFile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o644) //nolint:gosec
if err != nil {
// log disabled
return func() error { return nil }, nil //nolint:nilerr
}
log.SetOutput(f)
log.SetLevel(log.DebugLevel)
return f.Close, nil
}

351
main.go
View file

@ -1,9 +1,11 @@
// Package main provides the entry point for the Glow CLI application.
package main
import (
"errors"
"fmt"
"io"
"io/fs"
"net/http"
"net/url"
"os"
@ -11,11 +13,15 @@ import (
"path/filepath"
"strings"
tea "github.com/charmbracelet/bubbletea"
"mvdan.cc/sh/v3/shell"
"github.com/caarlos0/env/v11"
"github.com/charmbracelet/glamour"
"github.com/charmbracelet/glow/ui"
"github.com/charmbracelet/glow/utils"
"github.com/meowgorithm/babyenv"
"github.com/charmbracelet/glamour/styles"
"github.com/charmbracelet/glow/v2/ui"
"github.com/charmbracelet/glow/v2/utils"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/log"
gap "github.com/muesli/go-app-paths"
"github.com/spf13/cobra"
"github.com/spf13/viper"
@ -28,23 +34,34 @@ var (
// CommitSHA as provided by goreleaser.
CommitSHA = ""
readmeNames = []string{"README.md", "README"}
configFile string
pager bool
style string
width uint
showAllFiles bool
localOnly bool
mouse bool
readmeNames = []string{"README.md", "README", "Readme.md", "Readme", "readme.md", "readme"}
configFile string
pager bool
tui bool
style string
width uint
showAllFiles bool
showLineNumbers bool
preserveNewLines bool
mouse bool
rootCmd = &cobra.Command{
Use: "glow [SOURCE|DIR]",
Short: "Render markdown on the CLI, with pizzazz!",
Long: paragraph(fmt.Sprintf("\nRender markdown on the CLI, %s!", keyword("with pizzazz"))),
Use: "glow [SOURCE|DIR]",
Short: "Render markdown on the CLI, with pizzazz!",
Long: paragraph(
fmt.Sprintf("\nRender markdown on the CLI, %s!", keyword("with pizzazz")),
),
SilenceErrors: false,
SilenceUsage: false,
SilenceUsage: true,
TraverseChildren: true,
RunE: execute,
Args: cobra.MaximumNArgs(1),
ValidArgsFunction: func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
return nil, cobra.ShellCompDirectiveDefault
},
PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
return validateOptions(cmd)
},
RunE: execute,
}
)
@ -62,31 +79,22 @@ func sourceFromArg(arg string) (*source, error) {
}
// a GitHub or GitLab URL (even without the protocol):
if u, ok := isGitHubURL(arg); ok {
src, err := findGitHubREADME(u)
if err != nil {
return nil, err
}
return src, nil
}
if u, ok := isGitLabURL(arg); ok {
src, err := findGitLabREADME(u)
if err != nil {
return nil, err
}
src, err := readmeURL(arg)
if src != nil && err == nil {
// if there's an error, try next methods...
return src, nil
}
// HTTP(S) URLs:
if u, err := url.ParseRequestURI(arg); err == nil && strings.Contains(arg, "://") {
if u, err := url.ParseRequestURI(arg); err == nil && strings.Contains(arg, "://") { //nolint:nestif
if u.Scheme != "" {
if u.Scheme != "http" && u.Scheme != "https" {
return nil, fmt.Errorf("%s is not a supported protocol", u.Scheme)
}
// consumer of the source is responsible for closing the ReadCloser.
resp, err := http.Get(u.String()) // nolint:bodyclose
resp, err := http.Get(u.String()) //nolint: noctx,bodyclose
if err != nil {
return nil, err
return nil, fmt.Errorf("unable to get url: %w", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("HTTP status %d", resp.StatusCode)
@ -101,9 +109,12 @@ func sourceFromArg(arg string) (*source, error) {
arg = "."
}
st, err := os.Stat(arg)
if err == nil && st.IsDir() {
if err == nil && st.IsDir() { //nolint:nestif
var src *source
_ = filepath.Walk(arg, func(path string, info os.FileInfo, err error) error {
_ = filepath.Walk(arg, func(path string, _ os.FileInfo, err error) error {
if err != nil {
return err
}
for _, v := range readmeNames {
if strings.EqualFold(filepath.Base(path), v) {
r, err := os.Open(path)
@ -128,28 +139,49 @@ func sourceFromArg(arg string) (*source, error) {
return nil, errors.New("missing markdown source")
}
// a file:
r, err := os.Open(arg)
u, _ := filepath.Abs(arg)
return &source{r, u}, err
if err != nil {
return nil, fmt.Errorf("unable to open file: %w", err)
}
u, err := filepath.Abs(arg)
if err != nil {
return nil, fmt.Errorf("unable to get absolute path: %w", err)
}
return &source{r, u}, nil
}
// validateStyle checks if the style is a default style, if not, checks that
// the custom style exists.
func validateStyle(style string) error {
if style != "auto" && styles.DefaultStyles[style] == nil {
style = utils.ExpandPath(style)
if _, err := os.Stat(style); errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("specified style does not exist: %s", style)
} else if err != nil {
return fmt.Errorf("unable to stat file: %w", err)
}
}
return nil
}
func validateOptions(cmd *cobra.Command) error {
// grab config values from Viper
width = viper.GetUint("width")
localOnly = viper.GetBool("local")
mouse = viper.GetBool("mouse")
pager = viper.GetBool("pager")
tui = viper.GetBool("tui")
showAllFiles = viper.GetBool("all")
preserveNewLines = viper.GetBool("preserveNewLines")
showLineNumbers = viper.GetBool("showLineNumbers")
if pager && tui {
return errors.New("cannot use both pager and tui")
}
// validate the glamour style
style = viper.GetString("style")
if style != "auto" && glamour.DefaultStyles[style] == nil {
style = utils.ExpandPath(style)
if _, err := os.Stat(style); os.IsNotExist(err) {
return fmt.Errorf("Specified style does not exist: %s", style)
} else if err != nil {
return err
}
if err := validateStyle(style); err != nil {
return err
}
isTerminal := term.IsTerminal(int(os.Stdout.Fd()))
@ -160,18 +192,20 @@ func validateOptions(cmd *cobra.Command) error {
}
// Detect terminal width
if isTerminal && width == 0 && !cmd.Flags().Changed("width") {
w, _, err := term.GetSize(int(os.Stdout.Fd()))
if err == nil {
width = uint(w)
}
if !cmd.Flags().Changed("width") { //nolint:nestif
if isTerminal && width == 0 {
w, _, err := term.GetSize(int(os.Stdout.Fd()))
if err == nil {
width = uint(w) //nolint:gosec
}
if width > 120 {
width = 120
if width > 120 {
width = 120
}
}
if width == 0 {
width = 80
}
}
if width == 0 {
width = 80
}
return nil
}
@ -179,7 +213,7 @@ func validateOptions(cmd *cobra.Command) error {
func stdinIsPipe() (bool, error) {
stat, err := os.Stdin.Stat()
if err != nil {
return false, err
return false, fmt.Errorf("unable to open file: %w", err)
}
if stat.Mode()&os.ModeCharDevice == 0 || stat.Size() > 0 {
return true, nil
@ -188,11 +222,6 @@ func stdinIsPipe() (bool, error) {
}
func execute(cmd *cobra.Command, args []string) error {
initConfig()
if err := validateOptions(cmd); err != nil {
return err
}
// if stdin is a pipe then use stdin for input. note that you can also
// explicitly use a - to read from stdin.
if yes, err := stdinIsPipe(); err != nil {
@ -206,7 +235,7 @@ func execute(cmd *cobra.Command, args []string) error {
switch len(args) {
// TUI running on cwd
case 0:
return runTUI("", false)
return runTUI("", "")
// TUI with possible dir argument
case 1:
@ -216,7 +245,7 @@ func execute(cmd *cobra.Command, args []string) error {
if err == nil && info.IsDir() {
p, err := filepath.Abs(args[0])
if err == nil {
return runTUI(p, false)
return runTUI(p, "")
}
}
fallthrough
@ -246,7 +275,7 @@ func executeArg(cmd *cobra.Command, arg string, w io.Writer) error {
func executeCLI(cmd *cobra.Command, src *source, w io.Writer) error {
b, err := io.ReadAll(src.reader)
if err != nil {
return err
return fmt.Errorf("unable to read from reader: %w", err)
}
b = utils.RemoveFrontmatter(b)
@ -259,103 +288,106 @@ func executeCLI(cmd *cobra.Command, src *source, w io.Writer) error {
baseURL = u.String() + "/"
}
// initialize glamour
var gs glamour.TermRendererOption
if style == "auto" {
gs = glamour.WithEnvironmentConfig()
} else {
gs = glamour.WithStylePath(style)
}
isCode := !utils.IsMarkdownFile(src.URL)
// initialize glamour
r, err := glamour.NewTermRenderer(
gs,
glamour.WithWordWrap(int(width)),
glamour.WithColorProfile(lipgloss.ColorProfile()),
utils.GlamourStyle(style, isCode),
glamour.WithWordWrap(int(width)), //nolint:gosec
glamour.WithBaseURL(baseURL),
glamour.WithPreservedNewLines(),
)
if err != nil {
return err
return fmt.Errorf("unable to create renderer: %w", err)
}
out, err := r.RenderBytes(b)
content := string(b)
ext := filepath.Ext(src.URL)
if isCode {
content = utils.WrapCodeBlock(string(b), ext)
}
out, err := r.Render(content)
if err != nil {
return err
}
// trim lines
lines := strings.Split(string(out), "\n")
var content string
for i, s := range lines {
content += strings.TrimSpace(s)
// don't add an artificial newline after the last split
if i+1 < len(lines) {
content += "\n"
}
return fmt.Errorf("unable to render markdown: %w", err)
}
// display
if pager || cmd.Flags().Changed("pager") {
switch {
case pager || cmd.Flags().Changed("pager"):
pagerCmd := os.Getenv("PAGER")
if pagerCmd == "" {
pagerCmd = "less -r"
}
pa := strings.Split(pagerCmd, " ")
c := exec.Command(pa[0], pa[1:]...) // nolint:gosec
c.Stdin = strings.NewReader(content)
fields, err := shell.Fields(pagerCmd, os.Getenv)
if err != nil || len(fields) == 0 {
return fmt.Errorf("unable to parse PAGER command: %s", pagerCmd)
}
c := exec.Command(fields[0], fields[1:]...) //nolint:gosec
c.Stdin = strings.NewReader(out)
c.Stdout = os.Stdout
return c.Run()
if err := c.Run(); err != nil {
return fmt.Errorf("unable to run command: %w", err)
}
return nil
case tui || cmd.Flags().Changed("tui"):
path := ""
if !isURL(src.URL) {
path = src.URL
}
return runTUI(path, content)
default:
if _, err = fmt.Fprint(w, out); err != nil {
return fmt.Errorf("unable to write to writer: %w", err)
}
return nil
}
fmt.Fprint(w, content)
return nil
}
func runTUI(workingDirectory string, stashedOnly bool) error {
func runTUI(path string, content string) error {
// Read environment to get debugging stuff
var cfg ui.Config
if err := babyenv.Parse(&cfg); err != nil {
cfg, err := env.ParseAs[ui.Config]()
if err != nil {
return fmt.Errorf("error parsing config: %v", err)
}
// Log to file, if set
if cfg.Logfile != "" {
f, err := tea.LogToFile(cfg.Logfile, "glow")
if err != nil {
return err
}
defer f.Close() //nolint:errcheck
// use style set in env, or auto if unset
if err := validateStyle(cfg.GlamourStyle); err != nil {
cfg.GlamourStyle = style
}
cfg.WorkingDirectory = workingDirectory
cfg.DocumentTypes = ui.NewDocTypeSet()
cfg.Path = path
cfg.ShowAllFiles = showAllFiles
cfg.ShowLineNumbers = showLineNumbers
cfg.GlamourMaxWidth = width
cfg.GlamourStyle = style
cfg.EnableMouse = mouse
if stashedOnly {
cfg.DocumentTypes.Add(ui.StashedDoc, ui.NewsDoc)
} else if localOnly {
cfg.DocumentTypes.Add(ui.LocalDoc)
}
cfg.PreserveNewLines = preserveNewLines
// Run Bubble Tea program
if _, err := ui.NewProgram(cfg).Run(); err != nil {
return err
if _, err := ui.NewProgram(cfg, content).Run(); err != nil {
return fmt.Errorf("unable to run tui program: %w", err)
}
return nil
}
func main() {
if err := rootCmd.Execute(); err != nil {
closer, err := setupLog()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
if err := rootCmd.Execute(); err != nil {
_ = closer()
os.Exit(1)
}
_ = closer()
}
func init() {
tryLoadConfigFromDefaultPlaces()
if len(CommitSHA) >= 7 {
vt := rootCmd.VersionTemplate()
rootCmd.SetVersionTemplate(vt[:len(vt)-1] + " (" + CommitSHA[0:7] + ")\n")
@ -364,63 +396,78 @@ func init() {
Version = "unknown (built from source)"
}
rootCmd.Version = Version
scope := gap.NewScope(gap.User, "glow")
defaultConfigFile, _ := scope.ConfigPath("glow.yml")
rootCmd.InitDefaultCompletionCmd()
// "Glow Classic" cli arguments
rootCmd.PersistentFlags().StringVar(&configFile, "config", "", fmt.Sprintf("config file (default %s)", defaultConfigFile))
rootCmd.PersistentFlags().StringVar(&configFile, "config", "", fmt.Sprintf("config file (default %s)", viper.GetViper().ConfigFileUsed()))
rootCmd.Flags().BoolVarP(&pager, "pager", "p", false, "display with pager")
rootCmd.Flags().StringVarP(&style, "style", "s", "auto", "style name or JSON path")
rootCmd.Flags().UintVarP(&width, "width", "w", 0, "word-wrap at width")
rootCmd.Flags().BoolVarP(&tui, "tui", "t", false, "display with tui")
rootCmd.Flags().StringVarP(&style, "style", "s", styles.AutoStyle, "style name or JSON path")
rootCmd.Flags().UintVarP(&width, "width", "w", 0, "word-wrap at width (set to 0 to disable)")
rootCmd.Flags().BoolVarP(&showAllFiles, "all", "a", false, "show system files and directories (TUI-mode only)")
rootCmd.Flags().BoolVarP(&localOnly, "local", "l", false, "show local files only; no network (TUI-mode only)")
rootCmd.Flags().BoolVarP(&showLineNumbers, "line-numbers", "l", false, "show line numbers (TUI-mode only)")
rootCmd.Flags().BoolVarP(&preserveNewLines, "preserve-new-lines", "n", false, "preserve newlines in the output")
rootCmd.Flags().BoolVarP(&mouse, "mouse", "m", false, "enable mouse wheel (TUI-mode only)")
_ = rootCmd.Flags().MarkHidden("mouse")
// Config bindings
_ = viper.BindPFlag("pager", rootCmd.Flags().Lookup("pager"))
_ = viper.BindPFlag("tui", rootCmd.Flags().Lookup("tui"))
_ = viper.BindPFlag("style", rootCmd.Flags().Lookup("style"))
_ = viper.BindPFlag("width", rootCmd.Flags().Lookup("width"))
_ = viper.BindPFlag("local", rootCmd.Flags().Lookup("local"))
_ = viper.BindPFlag("debug", rootCmd.Flags().Lookup("debug"))
_ = viper.BindPFlag("mouse", rootCmd.Flags().Lookup("mouse"))
viper.SetDefault("style", "auto")
_ = viper.BindPFlag("preserveNewLines", rootCmd.Flags().Lookup("preserve-new-lines"))
_ = viper.BindPFlag("showLineNumbers", rootCmd.Flags().Lookup("line-numbers"))
_ = viper.BindPFlag("all", rootCmd.Flags().Lookup("all"))
viper.SetDefault("style", styles.AutoStyle)
viper.SetDefault("width", 0)
viper.SetDefault("local", "false")
viper.SetDefault("all", true)
// Stash
stashCmd.PersistentFlags().StringVarP(&memo, "memo", "m", "", "memo/note for stashing")
rootCmd.AddCommand(stashCmd)
rootCmd.AddCommand(configCmd)
rootCmd.AddCommand(configCmd, manCmd)
}
func initConfig() {
if configFile != "" {
viper.SetConfigFile(configFile)
} else {
scope := gap.NewScope(gap.User, "glow")
dirs, err := scope.ConfigDirs()
if err != nil {
fmt.Println("Can't retrieve default config. Please manually pass a config file with '--config'")
os.Exit(1)
}
for _, v := range dirs {
viper.AddConfigPath(v)
}
viper.SetConfigName("glow")
viper.SetConfigType("yaml")
func tryLoadConfigFromDefaultPlaces() {
scope := gap.NewScope(gap.User, "glow")
dirs, err := scope.ConfigDirs()
if err != nil {
fmt.Println("Could not load find configuration directory.")
os.Exit(1)
}
if c := os.Getenv("XDG_CONFIG_HOME"); c != "" {
dirs = append([]string{filepath.Join(c, "glow")}, dirs...)
}
if c := os.Getenv("GLOW_CONFIG_HOME"); c != "" {
dirs = append([]string{c}, dirs...)
}
for _, v := range dirs {
viper.AddConfigPath(v)
}
viper.SetConfigName("glow")
viper.SetConfigType("yaml")
viper.SetEnvPrefix("glow")
viper.AutomaticEnv()
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
fmt.Println("Error parsing config:", err)
os.Exit(1)
log.Warn("Could not parse configuration file", "err", err)
}
}
// fmt.Println("Using config file:", viper.ConfigFileUsed())
if used := viper.ConfigFileUsed(); used != "" {
log.Debug("Using configuration file", "path", viper.ConfigFileUsed())
return
}
if viper.ConfigFileUsed() == "" {
configFile = filepath.Join(dirs[0], "glow.yml")
}
if err := ensureConfigFile(); err != nil {
log.Error("Could not create default configuration", "error", err)
}
}

29
man_cmd.go Normal file
View file

@ -0,0 +1,29 @@
package main
import (
"fmt"
"os"
mcobra "github.com/muesli/mango-cobra"
"github.com/muesli/roff"
"github.com/spf13/cobra"
)
var manCmd = &cobra.Command{
Use: "man",
Short: "Generates manpages",
SilenceUsage: true,
DisableFlagsInUseLine: true,
Hidden: true,
Args: cobra.NoArgs,
RunE: func(*cobra.Command, []string) error {
manPage, err := mcobra.NewManPage(1, rootCmd)
if err != nil {
return fmt.Errorf("unable to instantiate man page: %w", err)
}
if _, err := fmt.Fprint(os.Stdout, manPage.Build(roff.NewDocument())); err != nil {
return fmt.Errorf("unable to build man page: %w", err)
}
return nil
},
}

View file

@ -1,82 +0,0 @@
package main
import (
"fmt"
"io"
"log"
"os"
"path"
"strings"
"github.com/charmbracelet/charm"
"github.com/charmbracelet/lipgloss"
"github.com/spf13/cobra"
)
var (
memo string
dot = lipgloss.NewStyle().Foreground(lipgloss.Color("#04B575")).Render("•")
stashCmd = &cobra.Command{
Use: "stash [SOURCE]",
Hidden: false,
Short: "Stash a markdown",
Long: paragraph(fmt.Sprintf("\nDo %s stuff. Run with no arguments to browse your stash or pass a path to a markdown file to stash it.", keyword("stash"))),
Example: paragraph("glow stash\nglow stash README.md\nglow stash -m \"secret notes\" path/to/notes.md"),
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
initConfig()
if len(args) == 0 {
return runTUI("", true)
}
filePath := args[0]
if memo == "" {
memo = strings.Replace(path.Base(filePath), path.Ext(filePath), "", 1)
}
cc := initCharmClient()
f, err := os.Open(filePath)
if err != nil {
return fmt.Errorf("bad filename")
}
defer f.Close() //nolint:errcheck
b, err := io.ReadAll(f)
if err != nil {
return fmt.Errorf("error reading file")
}
_, err = cc.StashMarkdown(memo, string(b))
if err != nil {
return fmt.Errorf("error stashing markdown")
}
fmt.Println(dot + " Stashed!")
return nil
},
}
)
func getCharmConfig() *charm.Config {
cfg, err := charm.ConfigFromEnv()
if err != nil {
log.Fatal(err)
}
return cfg
}
func initCharmClient() *charm.Client {
cfg := getCharmConfig()
cc, err := charm.NewClient(cfg)
if err == charm.ErrMissingSSHAuth {
fmt.Println(paragraph("We had some trouble authenticating via SSH. If this continues to happen the Charm tool may be able to help you. More info at https://github.com/charmbracelet/charm."))
os.Exit(1)
} else if err != nil {
fmt.Println(err)
os.Exit(1)
}
return cc
}

View file

@ -1,13 +1,13 @@
package main
import . "github.com/charmbracelet/lipgloss" //nolint:revive
import "github.com/charmbracelet/lipgloss"
var (
keyword = NewStyle().
Foreground(AdaptiveColor{Light: "#04B575", Dark: "#04B575"}).
keyword = lipgloss.NewStyle().
Foreground(lipgloss.Color("#04B575")).
Render
paragraph = NewStyle().
paragraph = lipgloss.NewStyle().
Width(78).
Padding(0, 0, 0, 2).
Render

View file

@ -2,29 +2,19 @@ package ui
// Config contains TUI-specific configuration.
type Config struct {
ShowAllFiles bool
Gopath string `env:"GOPATH"`
HomeDir string `env:"HOME"`
GlamourMaxWidth uint
GlamourStyle string
EnableMouse bool
ShowAllFiles bool
ShowLineNumbers bool
Gopath string `env:"GOPATH"`
HomeDir string `env:"HOME"`
GlamourMaxWidth uint
GlamourStyle string `env:"GLAMOUR_STYLE"`
EnableMouse bool
PreserveNewLines bool
// Which directory should we start from?
WorkingDirectory string
// Which document types shall we show?
DocumentTypes DocTypeSet
// Working directory or file path
Path string
// For debugging the UI
Logfile string `env:"GLOW_LOGFILE"`
HighPerformancePager bool `env:"GLOW_HIGH_PERFORMANCE_PAGER" default:"true"`
GlamourEnabled bool `env:"GLOW_ENABLE_GLAMOUR" default:"true"`
}
func (c Config) localOnly() bool {
return c.DocumentTypes.Equals(NewDocTypeSet(LocalDoc))
}
func (c Config) stashedOnly() bool {
return c.DocumentTypes.Contains(StashedDoc) && !c.DocumentTypes.Contains(LocalDoc)
HighPerformancePager bool `env:"GLOW_HIGH_PERFORMANCE_PAGER" envDefault:"true"`
GlamourEnabled bool `env:"GLOW_ENABLE_GLAMOUR" envDefault:"true"`
}

View file

@ -1,8 +0,0 @@
//go:build !windows
// +build !windows
package ui
const (
pagerStashIcon = "🔒"
)

View file

@ -1,7 +0,0 @@
// +build windows
package ui
const (
pagerStashIcon = "•"
)

View file

@ -1,90 +0,0 @@
package ui
// DocType represents a type of markdown document.
type DocType int
// Available document types.
const (
NoDocType DocType = iota
LocalDoc
StashedDoc
ConvertedDoc
NewsDoc
)
func (d DocType) String() string {
return [...]string{
"none",
"local",
"stashed",
"converted",
"news",
}[d]
}
// DocTypeSet is a set (in the mathematic sense) of document types.
type DocTypeSet map[DocType]struct{}
// NewDocTypeSet returns a set of document types.
func NewDocTypeSet(t ...DocType) DocTypeSet {
d := DocTypeSet(make(map[DocType]struct{}))
if len(t) > 0 {
d.Add(t...)
}
return d
}
// Add adds a document type of the set.
func (d *DocTypeSet) Add(t ...DocType) int {
for _, v := range t {
(*d)[v] = struct{}{}
}
return len(*d)
}
// Contains returns whether or not the set contains the given DocTypes.
func (d DocTypeSet) Contains(m ...DocType) bool {
matches := 0
for _, t := range m {
if _, found := d[t]; found {
matches++
}
}
return matches == len(m)
}
// Difference return a DocumentType set that does not contain the given types.
func (d DocTypeSet) Difference(t ...DocType) DocTypeSet {
c := copyDocumentTypes(d)
for k := range c {
for _, docType := range t {
if k == docType {
delete(c, k)
break
}
}
}
return c
}
// Equals returns whether or not the two sets are equal.
func (d DocTypeSet) Equals(other DocTypeSet) bool {
return d.Contains(other.AsSlice()...) && len(d) == len(other)
}
// AsSlice returns the set as a slice of document types.
func (d DocTypeSet) AsSlice() (agg []DocType) {
for k := range d {
agg = append(agg, k)
}
return
}
// Return a copy of the given DoctTypes map.
func copyDocumentTypes(d DocTypeSet) DocTypeSet {
c := make(map[DocType]struct{})
for k, v := range d {
c[k] = v
}
return c
}

View file

@ -1,48 +0,0 @@
package ui
import (
"reflect"
"testing"
)
func TestDocTypeContains(t *testing.T) {
d := NewDocTypeSet(LocalDoc)
if !d.Contains(LocalDoc) {
t.Error("Contains reported it doesn't contain a value which it absolutely does contain")
}
if d.Contains(NewsDoc) {
t.Error("Contains reported the set contains a value it certainly does not")
}
}
func TestDocTypeDifference(t *testing.T) {
original := NewDocTypeSet(LocalDoc, StashedDoc, ConvertedDoc, NewsDoc)
difference := original.Difference(LocalDoc, NewsDoc)
expected := NewDocTypeSet(StashedDoc, ConvertedDoc)
// Make sure the difference operation worked
if !reflect.DeepEqual(difference, expected) {
t.Errorf("difference returned %+v; expected %+v", difference, expected)
}
// Make sure original set was not mutated
if reflect.DeepEqual(original, difference) {
t.Errorf("original set was mutated when it should not have been")
}
}
func TestDocTypeEquality(t *testing.T) {
a := NewDocTypeSet(LocalDoc, StashedDoc)
b := NewDocTypeSet(LocalDoc, StashedDoc)
c := NewDocTypeSet(LocalDoc)
if !a.Equals(b) {
t.Errorf("Equality test failed for %+v and %+v; expected true, got false", a, b)
}
if a.Equals(c) {
t.Errorf("Equality test failed for %+v and %+v; expected false, got true", a, c)
}
}

View file

@ -2,14 +2,18 @@ package ui
import (
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/glow/editor"
"github.com/charmbracelet/x/editor"
)
type editorFinishedMsg struct{ err error }
func openEditor(path string) tea.Cmd {
func openEditor(path string, lineno int) tea.Cmd {
cb := func(err error) tea.Msg {
return editorFinishedMsg{err}
}
return tea.ExecProcess(editor.Cmd(path), cb)
cmd, err := editor.Cmd("Glow", path, editor.LineNumber(uint(lineno))) //nolint:gosec
if err != nil {
return func() tea.Msg { return cb(err) }
}
return tea.ExecProcess(cmd, cb)
}

View file

@ -1,13 +1,14 @@
//go:build darwin
// +build darwin
package ui
import "path/filepath"
func ignorePatterns(m model) []string {
func ignorePatterns(m commonModel) []string {
return []string{
filepath.Join(m.common.cfg.HomeDir, "Library"),
m.common.cfg.Gopath,
filepath.Join(m.cfg.HomeDir, "Library"),
m.cfg.Gopath,
"node_modules",
".*",
}

View file

@ -3,9 +3,9 @@
package ui
func ignorePatterns(m model) []string {
func ignorePatterns(m commonModel) []string {
return []string{
m.common.cfg.Gopath,
m.cfg.Gopath,
"node_modules",
".*",
}

View file

@ -1,42 +1,19 @@
package ui
import (
"log"
"fmt"
"math"
"path"
"strings"
"time"
"unicode"
"github.com/charmbracelet/charm"
"github.com/charmbracelet/log"
"github.com/dustin/go-humanize"
"github.com/segmentio/ksuid"
"golang.org/x/text/runes"
"golang.org/x/text/transform"
"golang.org/x/text/unicode/norm"
)
// markdown wraps charm.Markdown.
type markdown struct {
docType DocType
// Stash identifier. This exists so we can keep track of documents stashed
// in-session as they relate to their original, non-stashed counterparts.
// All documents have a stashID, however when a document is stashed that
// document inherits the stashID of the original.
stashID ksuid.KSUID
// Unique identifier. Unlike the stash identifier, this value should always
// be unique so we can confidently find it an operate on it (versus stashID,
// which could match both an original or stashed document).
uniqueID ksuid.KSUID
// Some of the document's original values before this document was stashed.
// These are irrelevant if this document was not stashed in this session.
originalDocType DocType
originalTimestamp time.Time
originalNote string
// Full path of a local markdown file. Only relevant to local documents and
// those that have been stashed in this session.
localPath string
@ -46,102 +23,24 @@ type markdown struct {
// field is ephemeral, and should only be referenced during filtering.
filterValue string
charm.Markdown
}
func (m *markdown) generateIDs() {
if m.stashID.IsNil() {
m.stashID = ksuid.New()
}
m.uniqueID = ksuid.New()
}
// convertToStashed converts this document into its stashed state.
func (m *markdown) convertToStashed() {
if m.docType == ConvertedDoc {
if debug {
log.Println("not converting already converted document:", m)
}
return
}
m.originalDocType = m.docType
m.originalTimestamp = m.CreatedAt
m.originalNote = m.Note
if m.docType == LocalDoc {
m.Note = strings.Replace(path.Base(m.localPath), path.Ext(m.localPath), "", 1)
}
m.CreatedAt = time.Now()
m.docType = ConvertedDoc
}
// revert reverts this document from its stashed state.
func (m *markdown) revertFromStashed() {
if m.docType != ConvertedDoc {
log.Printf("not reverting document of type %s: %v", m.docType, m)
}
m.docType = m.originalDocType
m.CreatedAt = m.originalTimestamp
m.Note = m.originalNote
Body string
Note string
Modtime time.Time
}
// Generate the value we're doing to filter against.
func (m *markdown) buildFilterValue() {
note, err := normalize(m.Note)
if err != nil {
if debug {
log.Printf("error normalizing '%s': %v", m.Note, err)
}
log.Error("error normalizing", "note", m.Note, "error", err)
m.filterValue = m.Note
}
m.filterValue = note
}
// shouldSortAsLocal returns whether or not this markdown should be sorted as though
// it's a local markdown document.
func (m markdown) shouldSortAsLocal() bool {
return m.docType == LocalDoc || m.docType == ConvertedDoc
}
// Sort documents with local files first, then by date.
type markdownsByLocalFirst []*markdown
func (m markdownsByLocalFirst) Len() int { return len(m) }
func (m markdownsByLocalFirst) Swap(i, j int) { m[i], m[j] = m[j], m[i] }
func (m markdownsByLocalFirst) Less(i, j int) bool {
iIsLocal := m[i].shouldSortAsLocal()
jIsLocal := m[j].shouldSortAsLocal()
// Local files (and files that used to be local) come first
if iIsLocal && !jIsLocal {
return true
}
if !iIsLocal && jIsLocal {
return false
}
// If both are local files, sort by filename. Note that we should never
// hit equality here since two files can't have the same path.
if iIsLocal && jIsLocal {
return strings.Compare(m[i].localPath, m[j].localPath) == -1
}
// Neither are local files so sort by date descending
if !m[i].CreatedAt.Equal(m[j].CreatedAt) {
return m[i].CreatedAt.After(m[j].CreatedAt)
}
// If the times also match, sort by unqiue ID.
ids := []ksuid.KSUID{m[i].uniqueID, m[j].uniqueID}
ksuid.Sort(ids)
return ids[0] == m[i].uniqueID
}
func (m markdown) relativeTime() string {
return relativeTime(m.CreatedAt)
return relativeTime(m.Modtime)
}
// Normalize text to aid in the filtering process. In particular, we remove
@ -150,19 +49,10 @@ func (m markdown) relativeTime() string {
func normalize(in string) (string, error) {
t := transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)
out, _, err := transform.String(t, in)
return out, err
}
// wrapMarkdowns wraps a *charm.Markdown with a *markdown in order to add some
// extra metadata.
func wrapMarkdowns(t DocType, md []*charm.Markdown) (m []*markdown) {
for _, v := range md {
m = append(m, &markdown{
docType: t,
Markdown: *v,
})
if err != nil {
return "", fmt.Errorf("error normalizing: %w", err)
}
return m
return out, nil
}
// Return the time in a human-readable format relative to the current time.

View file

@ -2,25 +2,29 @@ package ui
import (
"fmt"
"log"
"math"
"path/filepath"
"strings"
"time"
"github.com/atotto/clipboard"
"github.com/charmbracelet/bubbles/spinner"
"github.com/charmbracelet/bubbles/textinput"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/charm"
"github.com/charmbracelet/glamour"
"github.com/charmbracelet/glow/v2/utils"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/log"
"github.com/fsnotify/fsnotify"
runewidth "github.com/mattn/go-runewidth"
"github.com/muesli/reflow/ansi"
"github.com/muesli/reflow/truncate"
"github.com/muesli/termenv"
)
const statusBarHeight = 1
const (
statusBarHeight = 1
lineNumberWidth = 4
)
var (
pagerHelpHeight int
@ -28,11 +32,7 @@ var (
mintGreen = lipgloss.AdaptiveColor{Light: "#89F0CB", Dark: "#89F0CB"}
darkGreen = lipgloss.AdaptiveColor{Light: "#1C8760", Dark: "#1C8760"}
noteHeading = lipgloss.NewStyle().
Foreground(cream).
Background(green).
Padding(0, 1).
Render("Set Memo")
lineNumberFg = lipgloss.AdaptiveColor{Light: "#656565", Dark: "#7D7D7D"}
statusBarNoteFg = lipgloss.AdaptiveColor{Light: "#656565", Dark: "#7D7D7D"}
statusBarBg = lipgloss.AdaptiveColor{Light: "#E6E6E6", Dark: "#242424"}
@ -52,21 +52,11 @@ var (
Background(lipgloss.AdaptiveColor{Light: "#DCDCDC", Dark: "#323232"}).
Render
statusBarStashDotStyle = lipgloss.NewStyle().
Foreground(green).
Background(statusBarBg).
Render
statusBarMessageStyle = lipgloss.NewStyle().
Foreground(mintGreen).
Background(darkGreen).
Render
statusBarMessageStashIconStyle = lipgloss.NewStyle().
Foreground(mintGreen).
Background(darkGreen).
Render
statusBarMessageScrollPosStyle = lipgloss.NewStyle().
Foreground(mintGreen).
Background(darkGreen).
@ -82,47 +72,28 @@ var (
Background(lipgloss.AdaptiveColor{Light: "#f2f2f2", Dark: "#1B1B1B"}).
Render
spinnerStyle = lipgloss.NewStyle().
Foreground(statusBarNoteFg).
Background(statusBarBg)
pagerNoteInputPromptStyle = lipgloss.NewStyle().
Foreground(darkGray).
Background(yellowGreen).
Padding(0, 1)
pagerNoteInputStyle = lipgloss.NewStyle().
Foreground(darkGray).
Background(yellowGreen)
pagerNoteInputCursorStyle = lipgloss.NewStyle().
Foreground(fuschia)
lineNumberStyle = lipgloss.NewStyle().
Foreground(lineNumberFg).
Render
)
type (
contentRenderedMsg string
noteSavedMsg *charm.Markdown
reloadMsg struct{}
)
type pagerState int
const (
pagerStateBrowse pagerState = iota
pagerStateSetNote
pagerStateStashing
pagerStateStashSuccess
pagerStateStatusMessage
)
type pagerModel struct {
common *commonModel
viewport viewport.Model
state pagerState
showHelp bool
textInput textinput.Model
spinner spinner.Model
spinnerStart time.Time
common *commonModel
viewport viewport.Model
state pagerState
showHelp bool
statusMessage string
statusMessageTimer *time.Timer
@ -131,9 +102,7 @@ type pagerModel struct {
// it here so we can re-render it on resize.
currentDocument markdown
// Newly stashed markdown. We store it here temporarily so we can replace
// currentDocument above after a stash.
stashedDocument *markdown
watcher *fsnotify.Watcher
}
func newPagerModel(common *commonModel) pagerModel {
@ -142,34 +111,18 @@ func newPagerModel(common *commonModel) pagerModel {
vp.YPosition = 0
vp.HighPerformanceRendering = config.HighPerformancePager
// Text input for notes/memos
ti := textinput.New()
ti.Prompt = " > "
ti.PromptStyle = pagerNoteInputPromptStyle
ti.TextStyle = pagerNoteInputStyle
ti.CursorStyle = pagerNoteInputCursorStyle
ti.CharLimit = noteCharacterLimit
ti.Focus()
// Text input for search
sp := spinner.New()
sp.Style = spinnerStyle
return pagerModel{
common: common,
state: pagerStateBrowse,
textInput: ti,
viewport: vp,
spinner: sp,
m := pagerModel{
common: common,
state: pagerStateBrowse,
viewport: vp,
}
m.initWatcher()
return m
}
func (m *pagerModel) setSize(w, h int) {
m.viewport.Width = w
m.viewport.Height = h - statusBarHeight
m.textInput.Width = w -
ansi.PrintableRuneWidth(noteHeading) -
ansi.PrintableRuneWidth(m.textInput.Prompt) - 1
if m.showHelp {
if pagerHelpHeight == 0 {
@ -191,13 +144,18 @@ func (m *pagerModel) toggleHelp() {
}
}
type pagerStatusMessage struct {
message string
isError bool
}
// Perform stuff that needs to happen after a successful markdown stash. Note
// that the the returned command should be sent back the through the pager
// that the returned command should be sent back the through the pager
// update function.
func (m *pagerModel) showStatusMessage(statusMessage string) tea.Cmd {
func (m *pagerModel) showStatusMessage(msg pagerStatusMessage) tea.Cmd {
// Show a success message to the user
m.state = pagerStateStatusMessage
m.statusMessage = statusMessage
m.statusMessage = msg.message
if m.statusMessageTimer != nil {
m.statusMessageTimer.Stop()
}
@ -207,6 +165,7 @@ func (m *pagerModel) showStatusMessage(statusMessage string) tea.Cmd {
}
func (m *pagerModel) unload() {
log.Debug("unload")
if m.showHelp {
m.toggleHelp()
}
@ -216,7 +175,7 @@ func (m *pagerModel) unload() {
m.state = pagerStateBrowse
m.viewport.SetContent("")
m.viewport.YOffset = 0
m.textInput.Reset()
m.unwatchFile()
}
func (m pagerModel) update(msg tea.Msg) (pagerModel, tea.Cmd) {
@ -227,134 +186,77 @@ func (m pagerModel) update(msg tea.Msg) (pagerModel, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch m.state {
case pagerStateSetNote:
switch msg.String() {
case keyEsc:
switch msg.String() {
case "q", keyEsc:
if m.state != pagerStateBrowse {
m.state = pagerStateBrowse
return m, nil
case keyEnter:
var cmd tea.Cmd
if m.textInput.Value() != m.currentDocument.Note { // don't update if the note didn't change
m.currentDocument.Note = m.textInput.Value() // update optimistically
cmd = saveDocumentNote(m.common.cc, m.currentDocument.ID, m.currentDocument.Note)
}
m.state = pagerStateBrowse
m.textInput.Reset()
return m, cmd
}
default:
switch msg.String() {
case "q", keyEsc:
if m.state != pagerStateBrowse {
m.state = pagerStateBrowse
return m, nil
}
case "home", "g":
m.viewport.GotoTop()
if m.viewport.HighPerformanceRendering {
cmds = append(cmds, viewport.Sync(m.viewport))
}
case "end", "G":
m.viewport.GotoBottom()
if m.viewport.HighPerformanceRendering {
cmds = append(cmds, viewport.Sync(m.viewport))
}
case "m":
isStashed := m.currentDocument.docType == StashedDoc ||
m.currentDocument.docType == ConvertedDoc
// Users can only set the note on user-stashed markdown
if !isStashed {
break
}
m.state = pagerStateSetNote
// Stop the timer for hiding a status message since changing
// the state above will have cleared it.
if m.statusMessageTimer != nil {
m.statusMessageTimer.Stop()
}
// Pre-populate note with existing value
if m.textInput.Value() == "" {
m.textInput.SetValue(m.currentDocument.Note)
m.textInput.CursorEnd()
}
return m, textinput.Blink
case "e":
if m.currentDocument.docType == LocalDoc {
return m, openEditor(m.currentDocument.localPath)
}
case "c":
err := clipboard.WriteAll(m.currentDocument.Body)
if err != nil {
cmds = append(cmds, m.showStatusMessage("Unable to copy contents"))
} else {
cmds = append(cmds, m.showStatusMessage("Copied contents"))
}
case "s":
if m.common.authStatus != authOK {
break
}
md := m.currentDocument
_, alreadyStashed := m.common.filesStashed[md.stashID]
if alreadyStashed {
cmds = append(cmds, m.showStatusMessage("Already stashed"))
break
}
// Stash a local document
if m.state != pagerStateStashing && stashableDocTypes.Contains(md.docType) {
m.state = pagerStateStashing
m.spinnerStart = time.Now()
cmds = append(
cmds,
stashDocument(m.common.cc, md),
m.spinner.Tick,
)
}
case "?":
m.toggleHelp()
if m.viewport.HighPerformanceRendering {
cmds = append(cmds, viewport.Sync(m.viewport))
}
case "home", "g":
m.viewport.GotoTop()
if m.viewport.HighPerformanceRendering {
cmds = append(cmds, viewport.Sync(m.viewport))
}
case "end", "G":
m.viewport.GotoBottom()
if m.viewport.HighPerformanceRendering {
cmds = append(cmds, viewport.Sync(m.viewport))
}
}
case spinner.TickMsg:
spinnerMinTimeout := m.spinnerStart.
Add(spinnerVisibilityTimeout).
Add(spinnerMinLifetime)
case "d":
m.viewport.HalfViewDown()
if m.viewport.HighPerformanceRendering {
cmds = append(cmds, viewport.Sync(m.viewport))
}
if m.state == pagerStateStashing || time.Now().Before(spinnerMinTimeout) {
// We're either still stashing or we haven't reached the spinner's
// full lifetime. In either case we need to spin the spinner
// irrespective of it's more fine-grained visibility rules.
var cmd tea.Cmd
m.spinner, cmd = m.spinner.Update(msg)
cmds = append(cmds, cmd)
} else if m.state == pagerStateStashSuccess {
// Successful stash. Stop spinning and update accordingly.
m.state = pagerStateBrowse
m.currentDocument = *m.stashedDocument
m.stashedDocument = nil
cmds = append(cmds, m.showStatusMessage("Stashed!"))
case "u":
m.viewport.HalfViewUp()
if m.viewport.HighPerformanceRendering {
cmds = append(cmds, viewport.Sync(m.viewport))
}
case "e":
lineno := int(math.RoundToEven(float64(m.viewport.TotalLineCount()) * m.viewport.ScrollPercent()))
if m.viewport.AtTop() {
lineno = 0
}
log.Info(
"opening editor",
"file", m.currentDocument.localPath,
"line", fmt.Sprintf("%d/%d", lineno, m.viewport.TotalLineCount()),
)
return m, openEditor(m.currentDocument.localPath, lineno)
case "c":
// Copy using OSC 52
termenv.Copy(m.currentDocument.Body)
// Copy using native system clipboard
_ = clipboard.WriteAll(m.currentDocument.Body)
cmds = append(cmds, m.showStatusMessage(pagerStatusMessage{"Copied contents", false}))
case "r":
return m, loadLocalMarkdown(&m.currentDocument)
case "?":
m.toggleHelp()
if m.viewport.HighPerformanceRendering {
cmds = append(cmds, viewport.Sync(m.viewport))
}
}
// Glow has rendered the content
case contentRenderedMsg:
log.Info("content rendered", "state", m.state)
m.setContent(string(msg))
if m.viewport.HighPerformanceRendering {
cmds = append(cmds, viewport.Sync(m.viewport))
}
cmds = append(cmds, m.watchFile)
// The file was changed on disk and we're reloading it
case reloadMsg:
return m, loadLocalMarkdown(&m.currentDocument)
// We've finished editing the document, potentially making changes. Let's
// retrieve the latest version of the document so that we display
@ -362,69 +264,27 @@ func (m pagerModel) update(msg tea.Msg) (pagerModel, tea.Cmd) {
case editorFinishedMsg:
return m, loadLocalMarkdown(&m.currentDocument)
// We've reveived terminal dimensions, either for the first time or
// We've received terminal dimensions, either for the first time or
// after a resize
case tea.WindowSizeMsg:
return m, renderWithGlamour(m, m.currentDocument.Body)
case stashSuccessMsg:
// Stashing was successful. Convert the loaded document to a stashed
// one and show a status message. Note that we're also handling this
// message in the main update function where we're adding this stashed
// item to the stash listing.
m.state = pagerStateStashSuccess
if !m.spinnerVisible() {
// The spinner has finished spinning, so tell the user the stash
// was successful.
m.state = pagerStateBrowse
m.currentDocument = markdown(msg)
cmds = append(cmds, m.showStatusMessage("Stashed!"))
} else {
// The spinner is still spinning, so just take note of the newly
// stashed document for now.
md := markdown(msg)
m.stashedDocument = &md
}
case stashFailMsg:
delete(m.common.filesStashed, msg.markdown.stashID)
case statusMessageTimeoutMsg:
m.state = pagerStateBrowse
}
switch m.state {
case pagerStateSetNote:
m.textInput, cmd = m.textInput.Update(msg)
cmds = append(cmds, cmd)
default:
m.viewport, cmd = m.viewport.Update(msg)
cmds = append(cmds, cmd)
}
m.viewport, cmd = m.viewport.Update(msg)
cmds = append(cmds, cmd)
return m, tea.Batch(cmds...)
}
// spinnerVisible returns whether or not the spinner should be drawn.
func (m pagerModel) spinnerVisible() bool {
windowStart := m.spinnerStart.Add(spinnerVisibilityTimeout)
windowEnd := windowStart.Add(spinnerMinLifetime)
now := time.Now()
return now.After(windowStart) && now.Before(windowEnd)
}
func (m pagerModel) View() string {
var b strings.Builder
fmt.Fprint(&b, m.viewport.View()+"\n")
// Footer
switch m.state {
case pagerStateSetNote:
m.setNoteView(&b)
default:
m.statusBarView(&b)
}
m.statusBarView(&b)
if m.showHelp {
fmt.Fprint(&b, "\n"+m.helpView())
@ -440,11 +300,10 @@ func (m pagerModel) statusBarView(b *strings.Builder) {
percentToStringMagnitude float64 = 100.0
)
isStashed := m.currentDocument.docType == StashedDoc || m.currentDocument.docType == ConvertedDoc
showStatusMessage := m.state == pagerStateStatusMessage
// Logo
logo := glowLogoView(" Glow ")
logo := glowLogoView()
// Scroll percent
percent := math.Max(minPercent, math.Min(maxPercent, m.viewport.ScrollPercent()))
@ -463,34 +322,16 @@ func (m pagerModel) statusBarView(b *strings.Builder) {
helpNote = statusBarHelpStyle(" ? Help ")
}
// Status indicator; spinner or stash dot
var statusIndicator string
if m.state == pagerStateStashing || m.state == pagerStateStashSuccess {
var spinner string
if m.spinnerVisible() {
spinner = m.spinner.View()
}
statusIndicator = statusBarNoteStyle(" ") + spinner
} else if isStashed && showStatusMessage {
statusIndicator = statusBarMessageStashIconStyle(" " + pagerStashIcon)
} else if isStashed {
statusIndicator = statusBarStashDotStyle(" " + pagerStashIcon)
}
// Note
var note string
if showStatusMessage {
note = m.statusMessage
} else {
note = m.currentDocument.Note
if len(note) == 0 {
note = "(No memo)"
}
}
note = truncate.StringWithTail(" "+note+" ", uint(max(0,
note = truncate.StringWithTail(" "+note+" ", uint(max(0, //nolint:gosec
m.common.width-
ansi.PrintableRuneWidth(logo)-
ansi.PrintableRuneWidth(statusIndicator)-
ansi.PrintableRuneWidth(scrollPercent)-
ansi.PrintableRuneWidth(helpNote),
)), ellipsis)
@ -504,7 +345,6 @@ func (m pagerModel) statusBarView(b *strings.Builder) {
padding := max(0,
m.common.width-
ansi.PrintableRuneWidth(logo)-
ansi.PrintableRuneWidth(statusIndicator)-
ansi.PrintableRuneWidth(note)-
ansi.PrintableRuneWidth(scrollPercent)-
ansi.PrintableRuneWidth(helpNote),
@ -516,9 +356,8 @@ func (m pagerModel) statusBarView(b *strings.Builder) {
emptySpace = statusBarNoteStyle(emptySpace)
}
fmt.Fprintf(b, "%s%s%s%s%s%s",
fmt.Fprintf(b, "%s%s%s%s%s",
logo,
statusIndicator,
note,
emptySpace,
scrollPercent,
@ -526,36 +365,17 @@ func (m pagerModel) statusBarView(b *strings.Builder) {
)
}
func (m pagerModel) setNoteView(b *strings.Builder) {
fmt.Fprint(b, noteHeading)
fmt.Fprint(b, m.textInput.View())
}
func (m pagerModel) helpView() (s string) {
memoOrStash := "m set memo"
if m.common.authStatus == authOK && m.currentDocument.docType == LocalDoc {
memoOrStash = "s stash this document"
}
editOrBlank := "e edit this document"
if m.currentDocument.docType != LocalDoc || m.currentDocument.localPath == "" {
editOrBlank = ""
}
col1 := []string{
"g/home go to top",
"G/end go to bottom",
"c copy contents",
editOrBlank,
memoOrStash,
"e edit this document",
"r reload this document",
"esc back to files",
"q quit",
}
if m.currentDocument.docType == NewsDoc {
deleteFromStringSlice(col1, 3)
}
s += "\n"
s += "k/↑ up " + col1[0] + "\n"
s += "j/↓ down " + col1[1] + "\n"
@ -591,9 +411,7 @@ func renderWithGlamour(m pagerModel, md string) tea.Cmd {
return func() tea.Msg {
s, err := glamourRender(m, md)
if err != nil {
if debug {
log.Println("error rendering with Glamour:", err)
}
log.Error("error rendering with Glamour", "error", err)
return errMsg{err}
}
return contentRenderedMsg(s)
@ -602,53 +420,116 @@ func renderWithGlamour(m pagerModel, md string) tea.Cmd {
// This is where the magic happens.
func glamourRender(m pagerModel, markdown string) (string, error) {
trunc := lipgloss.NewStyle().MaxWidth(m.viewport.Width - lineNumberWidth).Render
if !config.GlamourEnabled {
return markdown, nil
}
// initialize glamour
var gs glamour.TermRendererOption
if m.common.cfg.GlamourStyle == "auto" {
gs = glamour.WithAutoStyle()
} else {
gs = glamour.WithStylePath(m.common.cfg.GlamourStyle)
isCode := !utils.IsMarkdownFile(m.currentDocument.Note)
width := max(0, min(int(m.common.cfg.GlamourMaxWidth), m.viewport.Width)) //nolint:gosec
if isCode {
width = 0
}
width := max(0, min(int(m.common.cfg.GlamourMaxWidth), m.viewport.Width))
r, err := glamour.NewTermRenderer(
gs,
options := []glamour.TermRendererOption{
utils.GlamourStyle(m.common.cfg.GlamourStyle, isCode),
glamour.WithWordWrap(width),
)
}
if m.common.cfg.PreserveNewLines {
options = append(options, glamour.WithPreservedNewLines())
}
r, err := glamour.NewTermRenderer(options...)
if err != nil {
return "", err
return "", fmt.Errorf("error creating glamour renderer: %w", err)
}
if isCode {
markdown = utils.WrapCodeBlock(markdown, filepath.Ext(m.currentDocument.Note))
}
out, err := r.Render(markdown)
if err != nil {
return "", err
return "", fmt.Errorf("error rendering markdown: %w", err)
}
if isCode {
out = strings.TrimSpace(out)
}
// trim lines
lines := strings.Split(out, "\n")
var content string
var content strings.Builder
for i, s := range lines {
content += strings.TrimSpace(s)
if isCode || m.common.cfg.ShowLineNumbers {
content.WriteString(lineNumberStyle(fmt.Sprintf("%"+fmt.Sprint(lineNumberWidth)+"d", i+1)))
content.WriteString(trunc(s))
} else {
content.WriteString(s)
}
// don't add an artificial newline after the last split
if i+1 < len(lines) {
content += "\n"
content.WriteRune('\n')
}
}
return content, nil
return content.String(), nil
}
// ETC
// Note: this runs in linear time; O(n).
func deleteFromStringSlice(a []string, i int) []string {
copy(a[i:], a[i+1:])
a[len(a)-1] = ""
return a[:len(a)-1]
func (m *pagerModel) initWatcher() {
var err error
m.watcher, err = fsnotify.NewWatcher()
if err != nil {
log.Error("error creating fsnotify watcher", "error", err)
}
}
func (m *pagerModel) watchFile() tea.Msg {
dir := m.localDir()
if err := m.watcher.Add(dir); err != nil {
log.Error("error adding dir to fsnotify watcher", "error", err)
return nil
}
log.Info("fsnotify watching dir", "dir", dir)
for {
select {
case event, ok := <-m.watcher.Events:
if !ok || event.Name != m.currentDocument.localPath {
continue
}
if !event.Has(fsnotify.Write) && !event.Has(fsnotify.Create) {
continue
}
log.Debug("fsnotify event", "file", event.Name, "event", event.Op)
return reloadMsg{}
case err, ok := <-m.watcher.Errors:
if !ok {
continue
}
log.Debug("fsnotify error", "dir", dir, "error", err)
}
}
}
func (m *pagerModel) unwatchFile() {
dir := m.localDir()
err := m.watcher.Remove(dir)
if err == nil {
log.Debug("fsnotify dir unwatched", "dir", dir)
} else {
log.Error("fsnotify fail to unwatch dir", "dir", dir, "error", err)
}
}
func (m *pagerModel) localDir() string {
return filepath.Dir(m.currentDocument.localPath)
}

12
ui/sort.go Normal file
View file

@ -0,0 +1,12 @@
package ui
import (
"cmp"
"slices"
)
func sortMarkdowns(mds []*markdown) {
slices.SortStableFunc(mds, func(a, b *markdown) int {
return cmp.Compare(a.Note, b.Note)
})
}

File diff suppressed because it is too large Load diff

View file

@ -15,7 +15,7 @@ type helpEntry struct{ key, val string }
type helpColumn []helpEntry
// newHelpColumn creates a help column from pairs of string arguments
// represeting keys and values. If the arguments are not even (and therein
// representing keys and values. If the arguments are not even (and therein
// not every key has a matching value) the function will panic.
func newHelpColumn(pairs ...string) (h helpColumn) {
if len(pairs)%2 != 0 {
@ -99,18 +99,7 @@ func (m stashModel) helpView() (string, int) {
return m.renderHelp(h)
}
// Help for when we're interacting with a single document
switch m.selectionState {
case selectionSettingNote:
return m.renderHelp([]string{"enter", "confirm", "esc", "cancel"}, []string{"q", "quit"})
case selectionPromptingDelete:
return m.renderHelp([]string{"y", "delete", "n", "cancel"}, []string{"q", "quit"})
}
var (
isStashed bool
isStashable bool
isEditable bool
navHelp []string
filterHelp []string
selectionHelp []string
@ -119,13 +108,6 @@ func (m stashModel) helpView() (string, int) {
appHelp []string
)
if numDocs > 0 {
md := m.selectedMarkdown()
isStashed = md != nil && md.docType == StashedDoc
isStashable = md != nil && md.docType == LocalDoc && m.online()
isEditable = md != nil && md.docType == LocalDoc && md.localPath != ""
}
if numDocs > 0 && m.showFullHelp {
navHelp = []string{"enter", "open", "j/k ↑/↓", "choose"}
}
@ -143,27 +125,23 @@ func (m stashModel) helpView() (string, int) {
}
// If we're browsing a filtered set
if m.filterState == filterApplied {
filterHelp = []string{"/", "edit search", "esc", "clear search"}
if m.filterApplied() {
filterHelp = []string{"/", "edit search", "esc", "clear filter"}
} else {
filterHelp = []string{"/", "find"}
}
if isStashed {
selectionHelp = []string{"x", "delete", "m", "set memo"}
} else if isStashable {
selectionHelp = []string{"s", "stash"}
}
if isEditable {
editHelp = []string{"e", "edit"}
}
// If there are errors
if m.err != nil {
appHelp = append(appHelp, "!", "errors")
}
appHelp = append(appHelp, "r", "refresh")
if numDocs > 0 {
appHelp = append(appHelp, "e", "edit")
}
appHelp = append(appHelp, "q", "quit")
// Detailed help
@ -181,19 +159,21 @@ func (m stashModel) helpView() (string, int) {
return m.renderHelp(navHelp, filterHelp, selectionHelp, editHelp, sectionHelp, appHelp)
}
const minHelpViewHeight = 5
// renderHelp returns the rendered help view and associated line height for
// the given groups of help items.
func (m stashModel) renderHelp(groups ...[]string) (string, int) {
if m.showFullHelp {
str := m.fullHelpView(groups...)
numLines := strings.Count(str, "\n") + 1
return str, numLines
return str, max(numLines, minHelpViewHeight)
}
return m.miniHelpView(concatStringSlices(groups...)...), 1
}
// Builds the help view from various sections pieces, truncating it if the view
// would otherwise wrap to two lines. Help view entires should come in as pairs,
// would otherwise wrap to two lines. Help view entries should come in as pairs,
// with the first being the key and the second being the help text.
func (m stashModel) miniHelpView(entries ...string) string {
if len(entries) == 0 {
@ -219,19 +199,13 @@ func (m stashModel) miniHelpView(entries ...string) string {
k := entries[i]
v := entries[i+1]
switch k {
case "s":
k = greenFg(k)
v = semiDimGreenFg(v)
default:
k = grayFg(k)
v = midGrayFg(v)
}
k = grayFg(k)
v = midGrayFg(v)
next = fmt.Sprintf("%s %s", k, v)
if i < len(entries)-2 {
next += dividerDot
next += dividerDot.String()
}
// Only this (and the following) help text items if we have the

View file

@ -2,47 +2,31 @@ package ui
import (
"fmt"
"log"
"strings"
"github.com/charmbracelet/lipgloss"
"github.com/muesli/reflow/ansi"
"github.com/charmbracelet/log"
"github.com/muesli/reflow/truncate"
"github.com/sahilm/fuzzy"
)
const (
verticalLine = "│"
noMemoTitle = "No Memo"
fileListingStashIcon = "• "
)
func stashItemView(b *strings.Builder, m stashModel, index int, md *markdown) {
var (
truncateTo = uint(m.common.width - stashViewHorizontalPadding*2)
gutter string
title = md.Note
date = md.relativeTime()
icon = ""
truncateTo = uint(m.common.width - stashViewHorizontalPadding*2) //nolint:gosec
gutter string
title = truncate.StringWithTail(md.Note, truncateTo, ellipsis)
date = md.relativeTime()
editedBy = ""
hasEditedBy = false
icon = ""
separator = ""
)
switch md.docType {
case NewsDoc:
if title == "" {
title = "News"
} else {
title = truncate.StringWithTail(title, truncateTo, ellipsis)
}
case StashedDoc, ConvertedDoc:
icon = fileListingStashIcon
if title == "" {
title = noMemoTitle
}
title = truncate.StringWithTail(title, truncateTo-uint(ansi.PrintableRuneWidth(icon)), ellipsis)
default:
title = truncate.StringWithTail(title, truncateTo, ellipsis)
}
isSelected := index == m.cursor()
isFiltering := m.filterState == filtering
singleFilteredItem := isFiltering && len(m.getVisibleMarkdowns()) == 1
@ -50,89 +34,67 @@ func stashItemView(b *strings.Builder, m stashModel, index int, md *markdown) {
// If there are multiple items being filtered don't highlight a selected
// item in the results. If we've filtered down to one item, however,
// highlight that first item since pressing return will open it.
if isSelected && !isFiltering || singleFilteredItem {
if isSelected && !isFiltering || singleFilteredItem { //nolint:nestif
// Selected item
switch m.selectionState {
case selectionPromptingDelete:
gutter = faintRedFg(verticalLine)
icon = faintRedFg(icon)
title = redFg(title)
date = faintRedFg(date)
case selectionSettingNote:
gutter = dullYellowFg(verticalLine)
icon = ""
title = m.noteInput.View()
date = dullYellowFg(date)
default:
if m.common.latestFileStashed == md.stashID &&
m.statusMessage == stashingStatusMessage {
gutter = greenFg(verticalLine)
icon = dimGreenFg(icon)
title = greenFg(title)
date = semiDimGreenFg(date)
} else {
gutter = dullFuchsiaFg(verticalLine)
icon = dullFuchsiaFg(icon)
if m.currentSection().key == filterSection &&
m.filterState == filterApplied || singleFilteredItem {
s := lipgloss.NewStyle().Foreground(fuschia)
title = styleFilteredText(title, m.filterInput.Value(), s, s.Copy().Underline(true))
} else {
title = fuchsiaFg(title)
}
date = dullFuchsiaFg(date)
}
}
} else {
// Regular (non-selected) items
gutter = " "
if m.common.latestFileStashed == md.stashID &&
m.statusMessage == stashingStatusMessage {
if m.statusMessage == stashingStatusMessage {
gutter = greenFg(verticalLine)
icon = dimGreenFg(icon)
title = greenFg(title)
date = semiDimGreenFg(date)
} else if md.docType == NewsDoc {
if isFiltering && m.filterInput.Value() == "" {
title = dimIndigoFg(title)
date = dimSubtleIndigoFg(date)
editedBy = semiDimGreenFg(editedBy)
separator = semiDimGreenFg(separator)
} else {
gutter = dullFuchsiaFg(verticalLine)
if m.currentSection().key == filterSection &&
m.filterState == filterApplied || singleFilteredItem {
s := lipgloss.NewStyle().Foreground(fuchsia)
title = styleFilteredText(title, m.filterInput.Value(), s, s.Underline(true))
} else {
s := lipgloss.NewStyle().Foreground(indigo)
title = styleFilteredText(title, m.filterInput.Value(), s, s.Copy().Underline(true))
date = subtleIndigoFg(date)
title = fuchsiaFg(title)
icon = fuchsiaFg(icon)
}
date = dimFuchsiaFg(date)
editedBy = dimDullFuchsiaFg(editedBy)
separator = dullFuchsiaFg(separator)
}
} else {
gutter = " "
if m.statusMessage == stashingStatusMessage {
icon = dimGreenFg(icon)
title = greenFg(title)
date = semiDimGreenFg(date)
editedBy = semiDimGreenFg(editedBy)
separator = semiDimGreenFg(separator)
} else if isFiltering && m.filterInput.Value() == "" {
icon = dimGreenFg(icon)
if title == noMemoTitle {
title = dimBrightGrayFg(title)
} else {
title = dimNormalFg(title)
}
title = dimNormalFg(title)
date = dimBrightGrayFg(date)
editedBy = dimBrightGrayFg(editedBy)
separator = dimBrightGrayFg(separator)
} else {
icon = greenFg(icon)
if title == noMemoTitle {
title = brightGrayFg(title)
} else {
s := lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#1a1a1a", Dark: "#dddddd"})
title = styleFilteredText(title, m.filterInput.Value(), s, s.Copy().Underline(true))
}
date = brightGrayFg(date)
s := lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#1a1a1a", Dark: "#dddddd"})
title = styleFilteredText(title, m.filterInput.Value(), s, s.Underline(true))
date = grayFg(date)
editedBy = midGrayFg(editedBy)
separator = brightGrayFg(separator)
}
}
fmt.Fprintf(b, "%s %s%s\n", gutter, icon, title)
fmt.Fprintf(b, "%s %s%s%s%s\n", gutter, icon, separator, separator, title)
fmt.Fprintf(b, "%s %s", gutter, date)
if hasEditedBy {
fmt.Fprintf(b, " %s", editedBy)
}
}
func styleFilteredText(haystack, needles string, defaultStyle, matchedStyle lipgloss.Style) string {
b := strings.Builder{}
normalizedHay, err := normalize(haystack)
if err != nil && debug {
log.Printf("error normalizing '%s': %v", haystack, err)
if err != nil {
log.Error("error normalizing", "haystack", haystack, "error", err)
}
matches := fuzzy.Find(needles, []string{normalizedHay})

View file

@ -1,84 +1,46 @@
package ui
import . "github.com/charmbracelet/lipgloss" //nolint: revive
import "github.com/charmbracelet/lipgloss"
// Colors.
var (
normal = AdaptiveColor{Light: "#1A1A1A", Dark: "#dddddd"}
normalDim = AdaptiveColor{Light: "#A49FA5", Dark: "#777777"}
gray = AdaptiveColor{Light: "#909090", Dark: "#626262"}
midGray = AdaptiveColor{Light: "#B2B2B2", Dark: "#4A4A4A"}
darkGray = AdaptiveColor{Light: "#DDDADA", Dark: "#3C3C3C"}
brightGray = AdaptiveColor{Light: "#847A85", Dark: "#979797"}
dimBrightGray = AdaptiveColor{Light: "#C2B8C2", Dark: "#4D4D4D"}
indigo = AdaptiveColor{Light: "#5A56E0", Dark: "#7571F9"}
dimIndigo = AdaptiveColor{Light: "#9498FF", Dark: "#494690"}
subtleIndigo = AdaptiveColor{Light: "#7D79F6", Dark: "#514DC1"}
dimSubtleIndigo = AdaptiveColor{Light: "#BBBDFF", Dark: "#383584"}
cream = AdaptiveColor{Light: "#FFFDF5", Dark: "#FFFDF5"}
yellowGreen = AdaptiveColor{Light: "#04B575", Dark: "#ECFD65"}
dullYellowGreen = AdaptiveColor{Light: "#6BCB94", Dark: "#9BA92F"}
fuschia = AdaptiveColor{Light: "#EE6FF8", Dark: "#EE6FF8"}
dimFuchsia = AdaptiveColor{Light: "#F1A8FF", Dark: "#99519E"}
dullFuchsia = AdaptiveColor{Dark: "#AD58B4", Light: "#F793FF"}
dimDullFuchsia = AdaptiveColor{Light: "#F6C9FF", Dark: "#6B3A6F"}
green = Color("#04B575")
red = AdaptiveColor{Light: "#FF4672", Dark: "#ED567A"}
faintRed = AdaptiveColor{Light: "#FF6F91", Dark: "#C74665"}
semiDimGreen = AdaptiveColor{Light: "#35D79C", Dark: "#036B46"}
dimGreen = AdaptiveColor{Light: "#72D2B0", Dark: "#0B5137"}
normalDim = lipgloss.AdaptiveColor{Light: "#A49FA5", Dark: "#777777"}
gray = lipgloss.AdaptiveColor{Light: "#909090", Dark: "#626262"}
midGray = lipgloss.AdaptiveColor{Light: "#B2B2B2", Dark: "#4A4A4A"}
darkGray = lipgloss.AdaptiveColor{Light: "#DDDADA", Dark: "#3C3C3C"}
brightGray = lipgloss.AdaptiveColor{Light: "#847A85", Dark: "#979797"}
dimBrightGray = lipgloss.AdaptiveColor{Light: "#C2B8C2", Dark: "#4D4D4D"}
cream = lipgloss.AdaptiveColor{Light: "#FFFDF5", Dark: "#FFFDF5"}
yellowGreen = lipgloss.AdaptiveColor{Light: "#04B575", Dark: "#ECFD65"}
fuchsia = lipgloss.AdaptiveColor{Light: "#EE6FF8", Dark: "#EE6FF8"}
dimFuchsia = lipgloss.AdaptiveColor{Light: "#F1A8FF", Dark: "#99519E"}
dullFuchsia = lipgloss.AdaptiveColor{Dark: "#AD58B4", Light: "#F793FF"}
dimDullFuchsia = lipgloss.AdaptiveColor{Light: "#F6C9FF", Dark: "#7B4380"}
green = lipgloss.Color("#04B575")
red = lipgloss.AdaptiveColor{Light: "#FF4672", Dark: "#ED567A"}
semiDimGreen = lipgloss.AdaptiveColor{Light: "#35D79C", Dark: "#036B46"}
dimGreen = lipgloss.AdaptiveColor{Light: "#72D2B0", Dark: "#0B5137"}
)
// Ulimately, we'll transition to named styles.
// nolint:deadcode,unused,varcheck
var (
normalFg = NewStyle().Foreground(normal).Render
dimNormalFg = NewStyle().Foreground(normalDim).Render
brightGrayFg = NewStyle().Foreground(brightGray).Render
dimBrightGrayFg = NewStyle().Foreground(dimBrightGray).Render
grayFg = NewStyle().Foreground(gray).Render
midGrayFg = NewStyle().Foreground(midGray).Render
darkGrayFg = NewStyle().Foreground(darkGray).Render
greenFg = NewStyle().Foreground(green).Render
semiDimGreenFg = NewStyle().Foreground(semiDimGreen).Render
dimGreenFg = NewStyle().Foreground(dimGreen).Render
fuchsiaFg = NewStyle().Foreground(fuschia).Render
dimFuchsiaFg = NewStyle().Foreground(dimFuchsia).Render
dullFuchsiaFg = NewStyle().Foreground(dullFuchsia).Render
dimDullFuchsiaFg = NewStyle().Foreground(dimDullFuchsia).Render
indigoFg = NewStyle().Foreground(fuschia).Render
dimIndigoFg = NewStyle().Foreground(dimIndigo).Render
subtleIndigoFg = NewStyle().Foreground(subtleIndigo).Render
dimSubtleIndigoFg = NewStyle().Foreground(dimSubtleIndigo).Render
yellowFg = NewStyle().Foreground(yellowGreen).Render // renders light green on light backgrounds
dullYellowFg = NewStyle().Foreground(dullYellowGreen).Render // renders light green on light backgrounds
redFg = NewStyle().Foreground(red).Render
faintRedFg = NewStyle().Foreground(faintRed).Render
)
var (
tabStyle = NewStyle().
Foreground(AdaptiveColor{Light: "#909090", Dark: "#626262"})
selectedTabStyle = NewStyle().
Foreground(AdaptiveColor{Light: "#333333", Dark: "#979797"})
errorTitleStyle = NewStyle().
Foreground(cream).
Background(red).
Padding(0, 1)
subtleStyle = NewStyle().
Foreground(AdaptiveColor{Light: "#9B9B9B", Dark: "#5C5C5C"})
paginationStyle = subtleStyle.Copy()
dimNormalFg = lipgloss.NewStyle().Foreground(normalDim).Render
brightGrayFg = lipgloss.NewStyle().Foreground(brightGray).Render
dimBrightGrayFg = lipgloss.NewStyle().Foreground(dimBrightGray).Render
grayFg = lipgloss.NewStyle().Foreground(gray).Render
midGrayFg = lipgloss.NewStyle().Foreground(midGray).Render
darkGrayFg = lipgloss.NewStyle().Foreground(darkGray)
greenFg = lipgloss.NewStyle().Foreground(green).Render
semiDimGreenFg = lipgloss.NewStyle().Foreground(semiDimGreen).Render
dimGreenFg = lipgloss.NewStyle().Foreground(dimGreen).Render
fuchsiaFg = lipgloss.NewStyle().Foreground(fuchsia).Render
dimFuchsiaFg = lipgloss.NewStyle().Foreground(dimFuchsia).Render
dullFuchsiaFg = lipgloss.NewStyle().Foreground(dullFuchsia).Render
dimDullFuchsiaFg = lipgloss.NewStyle().Foreground(dimDullFuchsia).Render
redFg = lipgloss.NewStyle().Foreground(red).Render
tabStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#909090", Dark: "#626262"})
selectedTabStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#333333", Dark: "#979797"})
errorTitleStyle = lipgloss.NewStyle().Foreground(cream).Background(red).Padding(0, 1)
subtleStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#9B9B9B", Dark: "#5C5C5C"})
paginationStyle = subtleStyle
)

557
ui/ui.go
View file

@ -1,33 +1,24 @@
// Package ui provides the main UI for the glow application.
package ui
import (
"errors"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/charm"
"github.com/charmbracelet/charm/keygen"
"github.com/charmbracelet/glow/utils"
"github.com/charmbracelet/glamour/styles"
"github.com/charmbracelet/glow/v2/utils"
"github.com/charmbracelet/log"
"github.com/muesli/gitcha"
te "github.com/muesli/termenv"
"github.com/segmentio/ksuid"
)
const (
noteCharacterLimit = 256 // should match server
statusMessageTimeout = time.Second * 2 // how long to show status messages like "stashed!"
statusMessageTimeout = time.Second * 3 // how long to show status messages like "stashed!"
ellipsis = "…"
// Only show the spinner if it spins for at least this amount of time.
spinnerVisibilityTimeout = time.Millisecond * 140
// Minimum amount of time the spinner should be visible once it starts.
spinnerMinLifetime = time.Millisecond * 550
)
var (
@ -36,29 +27,25 @@ var (
markdownExtensions = []string{
"*.md", "*.mdown", "*.mkdn", "*.mkd", "*.markdown",
}
// True if we're logging to a file, in which case we'll log more stuff.
debug = false
// Types of documents we allow the user to stash.
stashableDocTypes = NewDocTypeSet(LocalDoc, NewsDoc)
)
// NewProgram returns a new Tea program.
func NewProgram(cfg Config) *tea.Program {
if cfg.Logfile != "" {
log.Println("-- Starting Glow ----------------")
log.Printf("High performance pager: %v", cfg.HighPerformancePager)
log.Printf("Glamour rendering: %v", cfg.GlamourEnabled)
log.Println("Bubble Tea now initializing...")
debug = true
}
func NewProgram(cfg Config, content string) *tea.Program {
log.Debug(
"Starting glow",
"high_perf_pager",
cfg.HighPerformancePager,
"glamour",
cfg.GlamourEnabled,
)
config = cfg
opts := []tea.ProgramOption{tea.WithAltScreen()}
if cfg.EnableMouse {
opts = append(opts, tea.WithMouseCellMotion())
}
return tea.NewProgram(newModel(cfg), opts...)
m := newModel(cfg, content)
return tea.NewProgram(m, opts...)
}
type errMsg struct{ err error }
@ -66,10 +53,6 @@ type errMsg struct{ err error }
func (e errMsg) Error() string { return e.err.Error() }
type (
newCharmClientMsg *charm.Client
sshAuthErrMsg struct{}
keygenFailedMsg struct{ err error }
keygenSuccessMsg struct{}
initLocalFileSearchMsg struct {
cwd string
ch chan gitcha.SearchResult
@ -79,19 +62,10 @@ type (
type (
foundLocalFileMsg gitcha.SearchResult
localFileSearchFinished struct{}
gotStashMsg []*charm.Markdown
stashLoadErrMsg struct{ err error }
gotNewsMsg []*charm.Markdown
statusMessageTimeoutMsg applicationContext
newsLoadErrMsg struct{ err error }
stashSuccessMsg markdown
stashFailMsg struct {
err error
markdown markdown
}
)
// applicationContext indicates the area of the application something appies
// applicationContext indicates the area of the application something applies
// to. Occasionally used as an argument to commands and messages.
type applicationContext int
@ -115,60 +89,18 @@ func (s state) String() string {
}[s]
}
type authStatus int
const (
authConnecting authStatus = iota
authOK
authFailed
)
func (s authStatus) String() string {
return map[authStatus]string{
authConnecting: "connecting",
authOK: "ok",
authFailed: "failed",
}[s]
}
type keygenState int
const (
keygenUnstarted keygenState = iota
keygenRunning
keygenFinished
)
// Common stuff we'll need to access in all models.
type commonModel struct {
cfg Config
cc *charm.Client
cwd string
authStatus authStatus
width int
height int
// Local IDs of files stashed this session. We treat this like a set,
// ignoring the value portion with an empty struct.
filesStashed map[ksuid.KSUID]struct{}
// ID of the most recently stashed markdown
latestFileStashed ksuid.KSUID
// Files currently being stashed. We remove files from this set once
// a stash operation has either succeeded or failed.
filesStashing map[ksuid.KSUID]struct{}
}
func (c commonModel) isStashing() bool {
return len(c.filesStashing) > 0
cfg Config
cwd string
width int
height int
}
type model struct {
common *commonModel
state state
keygenState keygenState
fatalErr error
common *commonModel
state state
fatalErr error
// Sub-models
stash stashModel
@ -189,7 +121,7 @@ func (m *model) unloadDocument() []tea.Cmd {
var batch []tea.Cmd
if m.pager.viewport.HighPerformanceRendering {
batch = append(batch, tea.ClearScrollArea)
batch = append(batch, tea.ClearScrollArea) //nolint:staticcheck
}
if !m.stash.shouldSpin() {
@ -198,48 +130,73 @@ func (m *model) unloadDocument() []tea.Cmd {
return batch
}
func newModel(cfg Config) tea.Model {
if cfg.GlamourStyle == "auto" {
func newModel(cfg Config, content string) tea.Model {
initSections()
if cfg.GlamourStyle == styles.AutoStyle {
if te.HasDarkBackground() {
cfg.GlamourStyle = "dark"
cfg.GlamourStyle = styles.DarkStyle
} else {
cfg.GlamourStyle = "light"
cfg.GlamourStyle = styles.LightStyle
}
}
if len(cfg.DocumentTypes) == 0 {
cfg.DocumentTypes.Add(LocalDoc, StashedDoc, ConvertedDoc, NewsDoc)
}
common := commonModel{
cfg: cfg,
authStatus: authConnecting,
filesStashed: make(map[ksuid.KSUID]struct{}),
filesStashing: make(map[ksuid.KSUID]struct{}),
cfg: cfg,
}
return model{
common: &common,
state: stateShowStash,
keygenState: keygenUnstarted,
pager: newPagerModel(&common),
stash: newStashModel(&common),
m := model{
common: &common,
state: stateShowStash,
pager: newPagerModel(&common),
stash: newStashModel(&common),
}
path := cfg.Path
if path == "" && content != "" {
m.state = stateShowDocument
m.pager.currentDocument = markdown{Body: content}
return m
}
if path == "" {
path = "."
}
info, err := os.Stat(path)
if err != nil {
log.Error("unable to stat file", "file", path, "error", err)
m.fatalErr = err
return m
}
if info.IsDir() {
m.state = stateShowStash
} else {
cwd, _ := os.Getwd()
m.state = stateShowDocument
m.pager.currentDocument = markdown{
localPath: path,
Note: stripAbsolutePath(path, cwd),
Modtime: info.ModTime(),
}
}
return m
}
func (m model) Init() tea.Cmd {
var cmds []tea.Cmd
d := m.common.cfg.DocumentTypes
cmds := []tea.Cmd{m.stash.spinner.Tick}
if d.Contains(StashedDoc) || d.Contains(NewsDoc) {
cmds = append(cmds,
newCharmClient,
m.stash.spinner.Tick,
)
}
if d.Contains(LocalDoc) {
cmds = append(cmds, findLocalFiles(m))
switch m.state {
case stateShowStash:
cmds = append(cmds, findLocalFiles(*m.common))
case stateShowDocument:
content, err := os.ReadFile(m.common.cfg.Path)
if err != nil {
log.Error("unable to read file", "file", m.common.cfg.Path, "error", err)
return func() tea.Msg { return errMsg{err} }
}
body := string(utils.RemoveFrontmatter(content))
cmds = append(cmds, renderWithGlamour(m.pager, body))
}
return tea.Batch(cmds...)
@ -259,43 +216,45 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.KeyMsg:
switch msg.String() {
case "esc":
if m.state == stateShowDocument {
if m.state == stateShowDocument || m.stash.viewState == stashStateLoadingDocument {
batch := m.unloadDocument()
return m, tea.Batch(batch...)
}
case "r":
var cmd tea.Cmd
if m.state == stateShowStash {
// pass through all keys if we're editing the filter
if m.stash.filterState == filtering {
m.stash, cmd = m.stash.update(msg)
return m, cmd
}
m.stash.markdowns = nil
return m, m.Init()
}
case "q":
var cmd tea.Cmd
switch m.state {
switch m.state { //nolint:exhaustive
case stateShowStash:
// pass through all keys if we're editing the filter
if m.stash.filterState == filtering || m.stash.selectionState == selectionSettingNote {
if m.stash.filterState == filtering {
m.stash, cmd = m.stash.update(msg)
return m, cmd
}
// Special cases for the pager
case stateShowDocument:
switch m.pager.state {
// If setting a note send all keys straight through
case pagerStateSetNote:
var batch []tea.Cmd
newPagerModel, cmd := m.pager.update(msg)
m.pager = newPagerModel
batch = append(batch, cmd)
return m, tea.Batch(batch...)
}
}
return m, tea.Quit
case "left", "h", "delete":
if m.state == stateShowDocument && m.pager.state != pagerStateSetNote {
if m.state == stateShowDocument {
cmds = append(cmds, m.unloadDocument()...)
return m, tea.Batch(cmds...)
}
case "ctrl+z":
return m, tea.Suspend
// Ctrl+C always quits no matter where in the application you are.
case "ctrl+c":
return m, tea.Quit
@ -313,66 +272,16 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.common.cwd = msg.cwd
cmds = append(cmds, findNextLocalFile(m))
case sshAuthErrMsg:
if m.keygenState != keygenFinished { // if we haven't run the keygen yet, do that
m.keygenState = keygenRunning
cmds = append(cmds, generateSSHKeys)
} else {
// The keygen ran but things still didn't work and we can't auth
m.common.authStatus = authFailed
m.stash.err = errors.New("SSH authentication failed; we tried ssh-agent, loading keys from disk, and generating SSH keys")
if debug {
log.Println("entering offline mode;", m.stash.err)
}
// Even though it failed, news/stash loading is finished
m.stash.loaded.Add(StashedDoc, NewsDoc)
}
case keygenFailedMsg:
// Keygen failed. That sucks.
m.common.authStatus = authFailed
m.stash.err = errors.New("could not authenticate; could not generate SSH keys")
if debug {
log.Println("entering offline mode;", m.stash.err)
}
m.keygenState = keygenFinished
// Even though it failed, news/stash loading is finished
m.stash.loaded.Add(StashedDoc, NewsDoc)
case keygenSuccessMsg:
// The keygen's done, so let's try initializing the charm client again
m.keygenState = keygenFinished
cmds = append(cmds, newCharmClient)
case newCharmClientMsg:
m.common.cc = msg
m.common.authStatus = authOK
cmds = append(cmds, loadStash(m.stash), loadNews(m.stash))
case stashLoadErrMsg:
m.common.authStatus = authFailed
case fetchedMarkdownMsg:
// We've loaded a markdown file's contents for rendering
m.pager.currentDocument = *msg
msg.Body = string(utils.RemoveFrontmatter([]byte(msg.Body)))
cmds = append(cmds, renderWithGlamour(m.pager, msg.Body))
body := string(utils.RemoveFrontmatter([]byte(msg.Body)))
cmds = append(cmds, renderWithGlamour(m.pager, body))
case contentRenderedMsg:
m.state = stateShowDocument
case noteSavedMsg:
// A note was saved to a document. This will have been done in the
// pager, so we'll need to find the corresponding note in the stash.
// So, pass the message to the stash for processing.
stashModel, cmd := m.stash.update(msg)
m.stash = stashModel
return m, cmd
case localFileSearchFinished, gotStashMsg, gotNewsMsg:
case localFileSearchFinished:
// Always pass these messages to the stash so we can keep it updated
// about network activity, even if the user isn't currently viewing
// the stash.
@ -391,35 +300,6 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
cmds = append(cmds, findNextLocalFile(m))
case stashSuccessMsg:
// Common handling that should happen regardless of application state
md := markdown(msg)
m.stash.addMarkdowns(&md)
m.common.filesStashed[msg.stashID] = struct{}{}
delete(m.common.filesStashing, md.stashID)
if m.stash.filterApplied() {
for _, v := range m.stash.filteredMarkdowns {
if v.stashID == msg.stashID && v.docType == ConvertedDoc {
// Add the server-side ID we got back so we can do things
// like rename and stash it.
v.ID = msg.ID
// Keep the unique ID in sync so we can do things like
// delete. Note that the markdown received a new unique ID
// when it was added to the file listing in
// stash.addMarkdowns.
v.uniqueID = md.uniqueID
break
}
}
}
case stashFailMsg:
// Common handling that should happen regardless of application state
delete(m.common.filesStashed, msg.markdown.stashID)
delete(m.common.filesStashing, msg.markdown.stashID)
case filteredMarkdownMsg:
if m.state == stateShowDocument {
newStashModel, cmd := m.stash.update(msg)
@ -449,7 +329,7 @@ func (m model) View() string {
return errorView(m.fatalErr, true)
}
switch m.state {
switch m.state { //nolint:exhaustive
case stateShowDocument:
return m.pager.View()
default:
@ -474,10 +354,11 @@ func errorView(err error, fatal bool) string {
// COMMANDS
func findLocalFiles(m model) tea.Cmd {
func findLocalFiles(m commonModel) tea.Cmd {
return func() tea.Msg {
log.Info("findLocalFiles")
var (
cwd = m.common.cfg.WorkingDirectory
cwd = m.cfg.Path
err error
)
@ -493,26 +374,22 @@ func findLocalFiles(m model) tea.Cmd {
// Note that this is one error check for both cases above
if err != nil {
if debug {
log.Println("error finding local files:", err)
}
log.Error("error finding local files", "error", err)
return errMsg{err}
}
if debug {
log.Println("local directory is:", cwd)
log.Debug("local directory is", "cwd", cwd)
// Switch between FindFiles and FindAllFiles to bypass .gitignore rules
var ch chan gitcha.SearchResult
if m.cfg.ShowAllFiles {
ch, err = gitcha.FindAllFilesExcept(cwd, markdownExtensions, nil)
} else {
ch, err = gitcha.FindFilesExcept(cwd, markdownExtensions, ignorePatterns(m))
}
var ignore []string
if !m.common.cfg.ShowAllFiles {
ignore = ignorePatterns(m)
}
ch, err := gitcha.FindFilesExcept(cwd, markdownExtensions, ignore)
if err != nil {
if debug {
log.Println("error finding local files:", err)
}
log.Error("error finding local files", "error", err)
return errMsg{err}
}
@ -529,186 +406,11 @@ func findNextLocalFile(m model) tea.Cmd {
return foundLocalFileMsg(res)
}
// We're done
if debug {
log.Println("local file search finished")
}
log.Debug("local file search finished")
return localFileSearchFinished{}
}
}
func newCharmClient() tea.Msg {
cfg, err := charm.ConfigFromEnv()
if err != nil {
return errMsg{err}
}
cc, err := charm.NewClient(cfg)
if err == charm.ErrMissingSSHAuth {
if debug {
log.Println("missing SSH auth:", err)
}
return sshAuthErrMsg{}
} else if err != nil {
if debug {
log.Println("error creating new charm client:", err)
}
return errMsg{err}
}
return newCharmClientMsg(cc)
}
func loadStash(m stashModel) tea.Cmd {
return func() tea.Msg {
if m.common.cc == nil {
err := errors.New("no charm client")
if debug {
log.Println("error loading stash:", err)
}
return stashLoadErrMsg{err}
}
stash, err := m.common.cc.GetStash(m.serverPage)
if err != nil {
if debug {
if _, ok := err.(charm.ErrAuthFailed); ok {
log.Println("auth failure while loading stash:", err)
} else {
log.Println("error loading stash:", err)
}
}
return stashLoadErrMsg{err}
}
if debug {
log.Println("loaded stash page", m.serverPage)
}
return gotStashMsg(stash)
}
}
func loadNews(m stashModel) tea.Cmd {
return func() tea.Msg {
if m.common.cc == nil {
err := errors.New("no charm client")
if debug {
log.Println("error loading news:", err)
}
return newsLoadErrMsg{err}
}
news, err := m.common.cc.GetNews(1) // just fetch the first page
if err != nil {
if debug {
log.Println("error loading news:", err)
}
return newsLoadErrMsg{err}
}
if debug {
log.Println("fetched news")
}
return gotNewsMsg(news)
}
}
func generateSSHKeys() tea.Msg {
if debug {
log.Println("running keygen...")
}
_, err := keygen.NewSSHKeyPair(nil)
if err != nil {
if debug {
log.Println("keygen failed:", err)
}
return keygenFailedMsg{err}
}
if debug {
log.Println("keys generated succcessfully")
}
return keygenSuccessMsg{}
}
func saveDocumentNote(cc *charm.Client, id int, note string) tea.Cmd {
if cc == nil {
return func() tea.Msg {
err := errors.New("can't set note; no charm client")
if debug {
log.Println("error saving note:", err)
}
return errMsg{err}
}
}
return func() tea.Msg {
if err := cc.SetMarkdownNote(id, note); err != nil {
if debug {
log.Println("error saving note:", err)
}
return errMsg{err}
}
return noteSavedMsg(&charm.Markdown{ID: id, Note: note})
}
}
func stashDocument(cc *charm.Client, md markdown) tea.Cmd {
return func() tea.Msg {
if cc == nil {
err := errors.New("can't stash; no charm client")
if debug {
log.Println("error stashing document:", err)
}
return stashFailMsg{err, md}
}
// Is the document missing a body? If so, it likely means it needs to
// be loaded. But...if it turns out the document body really is empty
// then we'll stash it anyway.
if len(md.Body) == 0 {
switch md.docType {
case LocalDoc:
data, err := os.ReadFile(md.localPath)
if err != nil {
if debug {
log.Println("error loading document body for stashing:", err)
}
return stashFailMsg{err, md}
}
md.Body = string(data)
case NewsDoc:
newMD, err := fetchMarkdown(cc, md.ID, md.docType)
if err != nil {
if debug {
log.Println(err)
}
return stashFailMsg{err, md}
}
md.Body = newMD.Body
default:
err := fmt.Errorf("user is attempting to stash an unsupported markdown type: %s", md.docType)
if debug {
log.Println(err)
}
return stashFailMsg{err, md}
}
}
newMd, err := cc.StashMarkdown(md.Note, md.Body)
if err != nil {
if debug {
log.Println("error stashing document:", err)
}
return stashFailMsg{err, md}
}
md.convertToStashed()
// The server sends the whole stashed document back, but we really just
// need to know the ID so we can operate on this newly stashed
// markdown.
md.ID = newMd.ID
return stashSuccessMsg(md)
}
}
func waitForStatusMessageTimeout(appCtx applicationContext, t *time.Timer) tea.Cmd {
return func() tea.Msg {
<-t.C
@ -722,20 +424,17 @@ func waitForStatusMessageTimeout(appCtx applicationContext, t *time.Timer) tea.C
// document. Note that we could be doing things like checking if the file is
// a directory, but we trust that gitcha has already done that.
func localFileToMarkdown(cwd string, res gitcha.SearchResult) *markdown {
md := &markdown{
docType: LocalDoc,
return &markdown{
localPath: res.Path,
Markdown: charm.Markdown{
Note: stripAbsolutePath(res.Path, cwd),
CreatedAt: res.Info.ModTime(),
},
Note: stripAbsolutePath(res.Path, cwd),
Modtime: res.Info.ModTime(),
}
return md
}
func stripAbsolutePath(fullPath, cwd string) string {
return strings.Replace(fullPath, cwd+string(os.PathSeparator), "", -1)
fp, _ := filepath.EvalSymlinks(fullPath)
cp, _ := filepath.EvalSymlinks(cwd)
return strings.ReplaceAll(fp, cp+string(os.PathSeparator), "")
}
// Lightweight version of reflow's indent function.
@ -751,17 +450,3 @@ func indent(s string, n int) string {
}
return b.String()
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func max(a, b int) int {
if a > b {
return a
}
return b
}

86
url.go Normal file
View file

@ -0,0 +1,86 @@
package main
import (
"fmt"
"net/url"
"strings"
"sync"
)
const (
protoGithub = "github://"
protoGitlab = "gitlab://"
protoHTTPS = "https://"
)
var (
githubURL *url.URL
gitlabURL *url.URL
urlsOnce sync.Once
)
func init() {
urlsOnce.Do(func() {
githubURL, _ = url.Parse("https://github.com")
gitlabURL, _ = url.Parse("https://gitlab.com")
})
}
func readmeURL(path string) (*source, error) {
switch {
case strings.HasPrefix(path, protoGithub):
if u := githubReadmeURL(path); u != nil {
return readmeURL(u.String())
}
return nil, nil
case strings.HasPrefix(path, protoGitlab):
if u := gitlabReadmeURL(path); u != nil {
return readmeURL(u.String())
}
return nil, nil
}
if !strings.HasPrefix(path, protoHTTPS) {
path = protoHTTPS + path
}
u, err := url.Parse(path)
if err != nil {
return nil, fmt.Errorf("unable to parse url: %w", err)
}
switch {
case u.Hostname() == githubURL.Hostname():
return findGitHubREADME(u)
case u.Hostname() == gitlabURL.Hostname():
return findGitLabREADME(u)
}
return nil, nil
}
func githubReadmeURL(path string) *url.URL {
path = strings.TrimPrefix(path, protoGithub)
parts := strings.Split(path, "/")
if len(parts) != 2 {
// custom hostnames are not supported yet
return nil
}
u, _ := url.Parse(githubURL.String())
return u.JoinPath(path)
}
func gitlabReadmeURL(path string) *url.URL {
path = strings.TrimPrefix(path, protoGitlab)
parts := strings.Split(path, "/")
if len(parts) != 2 {
// custom hostnames are not supported yet
return nil
}
u, _ := url.Parse(gitlabURL.String())
return u.JoinPath(path)
}
func isURL(path string) bool {
_, err := url.ParseRequestURI(path)
return err == nil && strings.Contains(path, "://")
}

30
url_test.go Normal file
View file

@ -0,0 +1,30 @@
package main
import "testing"
func TestURLParser(t *testing.T) {
for path, url := range map[string]string{
"github.com/charmbracelet/glow": "https://raw.githubusercontent.com/charmbracelet/glow/master/README.md",
"github://charmbracelet/glow": "https://raw.githubusercontent.com/charmbracelet/glow/master/README.md",
"github://caarlos0/dotfiles.fish": "https://raw.githubusercontent.com/caarlos0/dotfiles.fish/main/README.md",
"github://tj/git-extras": "https://raw.githubusercontent.com/tj/git-extras/main/Readme.md",
"https://github.com/goreleaser/nfpm": "https://raw.githubusercontent.com/goreleaser/nfpm/main/README.md",
"gitlab.com/caarlos0/test": "https://gitlab.com/caarlos0/test/-/raw/master/README.md",
"gitlab://caarlos0/test": "https://gitlab.com/caarlos0/test/-/raw/master/README.md",
"https://gitlab.com/terrakok/gitlab-client": "https://gitlab.com/terrakok/gitlab-client/-/raw/develop/Readme.md",
} {
t.Run(path, func(t *testing.T) {
t.Skip("test uses network, sometimes fails for no reason")
got, err := readmeURL(path)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if got == nil {
t.Fatalf("should not be nil")
}
if url != got.URL {
t.Errorf("expected url for %s to be %s, was %s", path, url, got.URL)
}
})
}
}

View file

@ -1,9 +1,16 @@
// Package utils provides utility functions.
package utils
import (
"os"
"path/filepath"
"regexp"
"strings"
"github.com/charmbracelet/glamour"
"github.com/charmbracelet/glamour/ansi"
"github.com/charmbracelet/glamour/styles"
"github.com/charmbracelet/lipgloss"
"github.com/mitchellh/go-homedir"
)
@ -24,7 +31,7 @@ func detectFrontmatter(c []byte) []int {
return []int{-1, -1}
}
// Expands tilde and all environment variables from the given path.
// ExpandPath expands tilde and all environment variables from the given path.
func ExpandPath(path string) string {
s, err := homedir.Expand(path)
if err == nil {
@ -32,3 +39,75 @@ func ExpandPath(path string) string {
}
return os.ExpandEnv(path)
}
// WrapCodeBlock wraps a string in a code block with the given language.
func WrapCodeBlock(s, language string) string {
return "```" + language + "\n" + s + "```"
}
var markdownExtensions = []string{
".md", ".mdown", ".mkdn", ".mkd", ".markdown",
}
// IsMarkdownFile returns whether the filename has a markdown extension.
func IsMarkdownFile(filename string) bool {
ext := filepath.Ext(filename)
if ext == "" {
// By default, assume it's a markdown file.
return true
}
for _, v := range markdownExtensions {
if strings.EqualFold(ext, v) {
return true
}
}
// Has an extension but not markdown
// so assume this is a code file.
return false
}
// GlamourStyle returns a glamour.TermRendererOption based on the given style.
func GlamourStyle(style string, isCode bool) glamour.TermRendererOption {
if !isCode {
if style == styles.AutoStyle {
return glamour.WithAutoStyle()
}
return glamour.WithStylePath(style)
}
// If we are rendering a pure code block, we need to modify the style to
// remove the indentation.
var styleConfig ansi.StyleConfig
switch style {
case styles.AutoStyle:
if lipgloss.HasDarkBackground() {
styleConfig = styles.DarkStyleConfig
} else {
styleConfig = styles.LightStyleConfig
}
case styles.DarkStyle:
styleConfig = styles.DarkStyleConfig
case styles.LightStyle:
styleConfig = styles.LightStyleConfig
case styles.PinkStyle:
styleConfig = styles.PinkStyleConfig
case styles.NoTTYStyle:
styleConfig = styles.NoTTYStyleConfig
case styles.DraculaStyle:
styleConfig = styles.DraculaStyleConfig
case styles.TokyoNightStyle:
styleConfig = styles.DraculaStyleConfig
default:
return glamour.WithStylesFromJSONFile(style)
}
var margin uint
styleConfig.CodeBlock.Margin = &margin
return glamour.WithStyles(styleConfig)
}