Skip to content

Commit

Permalink
feat(font): cache zip files
Browse files Browse the repository at this point in the history
  • Loading branch information
JanDeDobbeleer committed Dec 19, 2024
1 parent 4d06374 commit 7d7869b
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 54 deletions.
91 changes: 59 additions & 32 deletions src/font/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,13 @@ const (
)

type main struct {
err error
list *list.Model
font string
zipFolder string
families []string
spinner spinner.Model
state state
system bool
err error
list *list.Model
Asset
families []string
spinner spinner.Model
state state
system bool
}

func (m *main) buildFontList(nerdFonts []*Asset) {
Expand Down Expand Up @@ -122,7 +121,7 @@ func downloadFontZip(location string) {
}

func installLocalFontZIP(m *main) {
data, err := os.ReadFile(m.font)
data, err := os.ReadFile(m.URL)
if err != nil {
program.Send(errMsg(err))
return
Expand All @@ -143,28 +142,48 @@ func installFontZIP(zipFile []byte, m *main) {

func (m *main) Init() tea.Cmd {
isLocalZipFile := func() bool {
return !strings.HasPrefix(m.font, "https") && strings.HasSuffix(m.font, ".zip")
return !strings.HasPrefix(m.URL, "https") && strings.HasSuffix(m.URL, ".zip")
}

if len(m.font) != 0 && !isLocalZipFile() {
m.state = downloadFont
resolveFontZipURL := func() error {
if strings.HasPrefix(m.URL, "https") {
return nil
}

if !strings.HasPrefix(m.font, "https") {
if m.font == CascadiaCodeMS {
cascadia, err := CascadiaCode()
if err != nil {
m.err = err
return tea.Quit
}
fonts, err := Fonts()
if err != nil {
return err
}

m.font = cascadia.URL
} else {
m.font = fmt.Sprintf("https://github.com/ryanoasis/nerd-fonts/releases/latest/download/%s.zip", m.font)
var fontAsset *Asset
for _, font := range fonts {
if m.URL != font.Name {
continue
}

fontAsset = font
break
}

if fontAsset == nil {
return fmt.Errorf("no matching font found")
}

m.Asset = *fontAsset

return nil
}

if len(m.URL) != 0 && !isLocalZipFile() {
m.state = downloadFont

if err := resolveFontZipURL(); err != nil {
m.err = err
return tea.Quit
}

defer func() {
go downloadFontZip(m.font)
go downloadFontZip(m.URL)
}()

m.spinner.Spinner = spinner.Globe
Expand Down Expand Up @@ -214,20 +233,25 @@ func (m *main) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, tea.Quit

case "enter":
if len(m.font) != 0 || m.list == nil || m.list.SelectedItem() == nil {
if len(m.URL) != 0 || m.list == nil || m.list.SelectedItem() == nil {
return m, nil
}

var font *Asset
var ok bool

if font, ok = m.list.SelectedItem().(*Asset); !ok {
m.err = fmt.Errorf("no font selected")
return m, tea.Quit
}

m.state = downloadFont
m.font = font.Name
m.Asset = *font

defer func() {
go downloadFontZip(font.URL)
}()

m.spinner.Spinner = spinner.Globe
return m, m.spinner.Tick

Expand Down Expand Up @@ -295,11 +319,11 @@ func (m *main) View() string {
case selectFont:
return fmt.Sprintf("\n%s%s", m.list.View(), terminal.StopProgress())
case downloadFont:
return textStyle.Render(fmt.Sprintf("%s Downloading %s%s", m.spinner.View(), m.font, terminal.StartProgress()))
return textStyle.Render(fmt.Sprintf("%s Downloading %s%s", m.spinner.View(), m.Name, terminal.StartProgress()))
case unzipFont:
return textStyle.Render(fmt.Sprintf("%s Extracting %s", m.spinner.View(), m.font))
return textStyle.Render(fmt.Sprintf("%s Extracting %s", m.spinner.View(), m.Name))
case installFont:
return textStyle.Render(fmt.Sprintf("%s Installing %s", m.spinner.View(), m.font))
return textStyle.Render(fmt.Sprintf("%s Installing %s", m.spinner.View(), m.Name))
case quit:
return textStyle.Render(fmt.Sprintf("No need to install a new font? That's cool.%s", terminal.StopProgress()))
case done:
Expand All @@ -309,7 +333,7 @@ func (m *main) View() string {

var builder strings.Builder

builder.WriteString(fmt.Sprintf("Successfully installed %s 🚀\n\n%s", m.font, terminal.StopProgress()))
builder.WriteString(fmt.Sprintf("Successfully installed %s 🚀\n\n%s", m.Name, terminal.StopProgress()))
builder.WriteString("The following font families are now available for configuration:\n\n")

for i, family := range m.families {
Expand All @@ -328,9 +352,12 @@ func (m *main) View() string {

func Run(font string, ch cache_.Cache, root bool, zipFolder string) {
main := &main{
font: font,
system: root,
zipFolder: zipFolder,
system: root,
Asset: Asset{
Name: font,
URL: font,
Folder: zipFolder,
},
}

cache = ch
Expand Down
40 changes: 36 additions & 4 deletions src/font/download.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,55 @@ import (
"io"
httplib "net/http"
"net/url"
"os"
"path"
"path/filepath"

cache_ "github.com/jandedobbeleer/oh-my-posh/src/cache"
"github.com/jandedobbeleer/oh-my-posh/src/runtime/http"
)

func Download(fontPath string) ([]byte, error) {
u, err := url.Parse(fontPath)
func Download(fontURL string) ([]byte, error) {
if zipPath, OK := cache.Get(fontURL); OK {
if b, err := os.ReadFile(zipPath); err == nil {
return b, nil
}
}

// validate if we have a local file
u, err := url.Parse(fontURL)
if err != nil || u.Scheme != "https" {
return nil, errors.New("font path must be a valid URL")
}

var b []byte
if b, err = getRemoteFile(fontPath); err != nil {
if b, err = getRemoteFile(fontURL); err != nil {
return nil, err
}

if !isZipFile(b) {
return nil, fmt.Errorf("%s is not a valid zip file", fontPath)
return nil, fmt.Errorf("%s is not a valid zip file", fontURL)
}

fileName := path.Base(fontURL)

zipPath := filepath.Join(os.TempDir(), fileName)
tempFile, err := os.Create(zipPath)
defer func() {
_ = tempFile.Close()
}()

if err != nil {
return b, nil
}

_, err = tempFile.Write(b)
if err != nil {
return b, nil
}

cache.Set(fontURL, zipPath, cache_.ONEDAY)

return b, nil
}

Expand All @@ -42,10 +72,12 @@ func getRemoteFile(location string) (data []byte, err error) {
if err != nil {
return nil, err
}

resp, err := http.HTTPClient.Do(req)
if err != nil {
return
}

defer resp.Body.Close()

if resp.StatusCode != httplib.StatusOK {
Expand Down
42 changes: 33 additions & 9 deletions src/font/fonts.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ type release struct {
}

type Asset struct {
Name string `json:"name"`
URL string `json:"browser_download_url"`
State string `json:"state"`
Name string `json:"name"`
URL string `json:"browser_download_url"`
State string `json:"state"`
Folder string `json:"folder"`
}

func (a Asset) FilterValue() string { return a.Name }
Expand All @@ -42,7 +43,7 @@ func Fonts() ([]*Asset, error) {

cascadiaCode, err := CascadiaCode()
if err == nil {
assets = append(assets, cascadiaCode)
assets = append(assets, cascadiaCode...)
}

sort.Slice(assets, func(i, j int) bool { return assets[i].Name < assets[j].Name })
Expand Down Expand Up @@ -84,16 +85,39 @@ func setCachedFontData(assets []*Asset) {
cache.Set(cache_.FONTLISTCACHE, string(data), cache_.ONEDAY)
}

func CascadiaCode() (*Asset, error) {
func CascadiaCode() ([]*Asset, error) {
assets, err := fetchFontAssets("microsoft/cascadia-code")
if err != nil || len(assets) != 1 {
return nil, errors.New("no assets found")
}

// patch the name
assets[0].Name = CascadiaCodeMS

return assets[0], nil
return []*Asset{
{
Name: fmt.Sprintf("%s - TTF", CascadiaCodeMS),
URL: assets[0].URL,
Folder: "ttf/",
},
{
Name: fmt.Sprintf("%s - TTF Static", CascadiaCodeMS),
URL: assets[0].URL,
Folder: "ttf/static/",
},
{
Name: fmt.Sprintf("%s - OTF Static", CascadiaCodeMS),
URL: assets[0].URL,
Folder: "otf/static/",
},
{
Name: fmt.Sprintf("%s - WOFF2", CascadiaCodeMS),
URL: assets[0].URL,
Folder: "woff2/static/",
},
{
Name: fmt.Sprintf("%s - WOFF2 Static", CascadiaCodeMS),
URL: assets[0].URL,
Folder: "woff2/static/",
},
}, nil
}

func fetchFontAssets(repo string) ([]*Asset, error) {
Expand Down
2 changes: 1 addition & 1 deletion src/font/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func InstallZIP(data []byte, m *main) ([]string, error) {
}

fontFileName := path.Base(file.Name)
fontRelativeFileName := strings.TrimPrefix(file.Name, m.zipFolder)
fontRelativeFileName := strings.TrimPrefix(file.Name, m.Folder)

// do not install fonts that are not in the specified installation folder
if fontFileName != fontRelativeFileName {
Expand Down
4 changes: 2 additions & 2 deletions src/image/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import (
"unicode/utf8"

"github.com/jandedobbeleer/oh-my-posh/src/cache"
fontCLI "github.com/jandedobbeleer/oh-my-posh/src/font"
font_ "github.com/jandedobbeleer/oh-my-posh/src/font"
"github.com/jandedobbeleer/oh-my-posh/src/regex"
"github.com/jandedobbeleer/oh-my-posh/src/runtime"
"github.com/jandedobbeleer/oh-my-posh/src/terminal"
Expand Down Expand Up @@ -229,7 +229,7 @@ func (ir *Renderer) loadFonts() error {
url := "https://github.com/ryanoasis/nerd-fonts/releases/download/v3.2.1/Hack.zip"
var err error

data, err = fontCLI.Download(url)
data, err = font_.Download(url)
if err != nil {
return err
}
Expand Down
6 changes: 0 additions & 6 deletions website/docs/installation/fonts.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,6 @@ This will present a list of Nerd Font libraries, from which you can select `Mes
oh-my-posh font install meslo
```

If you have a font that has specific flavors of a font inside sub folders, you can specify the sub folder name:

```bash
oh-my-posh font install --zip-folder ttf/static "CascadiaCode (MS)"
```

</TabItem>
<TabItem value="homebrew">

Expand Down

0 comments on commit 7d7869b

Please sign in to comment.