feat(fonts): install as user

This commit is contained in:
Jan De Dobbeleer 2023-06-12 20:19:47 +02:00 committed by Jan De Dobbeleer
parent 5289ba1f19
commit eee5198664
7 changed files with 66 additions and 40 deletions

View file

@ -10,6 +10,8 @@ import (
) )
var ( var (
user bool
// fontCmd can work with fonts // fontCmd can work with fonts
fontCmd = &cobra.Command{ fontCmd = &cobra.Command{
Use: "font [install|configure]", Use: "font [install|configure]",
@ -37,8 +39,22 @@ This command is used to install fonts and configure the font in your terminal.
env := &platform.Shell{} env := &platform.Shell{}
env.Init() env.Init()
defer env.Close() defer env.Close()
needsAdmin := env.GOOS() == platform.WINDOWS && !env.Root()
font.Run(fontName, needsAdmin) // Windows users need to specify the --user flag if they want to install the font as user
// If the user does not specify the --user flag, the font will be installed as a system font
// and therefore we need to be administrator
system := env.Root()
if env.GOOS() == platform.WINDOWS && !user && !system {
fmt.Println(`
You need to be administrator to install a font as system font.
You can either run this command as administrator or specify the --user flag to install the font for your user only:
oh-my-posh font install --user
`)
return
}
font.Run(fontName, system)
return return
case "configure": case "configure":
fmt.Println("not implemented") fmt.Println("not implemented")
@ -51,4 +67,5 @@ This command is used to install fonts and configure the font in your terminal.
func init() { //nolint:gochecknoinits func init() { //nolint:gochecknoinits
RootCmd.AddCommand(fontCmd) RootCmd.AddCommand(fontCmd)
fontCmd.Flags().BoolVar(&user, "user", false, "install font as user")
} }

View file

@ -64,7 +64,6 @@ const (
downloadFont downloadFont
unzipFont unzipFont
installFont installFont
admin
quit quit
done done
) )
@ -72,7 +71,7 @@ const (
type main struct { type main struct {
spinner spinner.Model spinner spinner.Model
list *list.Model list *list.Model
needsAdmin bool system bool
font string font string
state state state state
err error err error
@ -115,17 +114,17 @@ func downloadFontZip(location string) {
program.Send(zipMsg(zipFile)) program.Send(zipMsg(zipFile))
} }
func installLocalFontZIP(zipFile string) { func installLocalFontZIP(zipFile string, user bool) {
data, err := os.ReadFile(zipFile) data, err := os.ReadFile(zipFile)
if err != nil { if err != nil {
program.Send(errMsg(err)) program.Send(errMsg(err))
return return
} }
installFontZIP(data) installFontZIP(data, user)
} }
func installFontZIP(zipFile []byte) { func installFontZIP(zipFile []byte, user bool) {
err := InstallZIP(zipFile) err := InstallZIP(zipFile, user)
if err != nil { if err != nil {
program.Send(errMsg(err)) program.Send(errMsg(err))
return return
@ -134,11 +133,6 @@ func installFontZIP(zipFile []byte) {
} }
func (m *main) Init() tea.Cmd { func (m *main) Init() tea.Cmd {
if m.needsAdmin {
m.state = admin
return tea.Quit
}
isLocalZipFile := func() bool { isLocalZipFile := func() bool {
return !strings.HasPrefix(m.font, "https") && strings.HasSuffix(m.font, ".zip") return !strings.HasPrefix(m.font, "https") && strings.HasSuffix(m.font, ".zip")
} }
@ -157,7 +151,7 @@ func (m *main) Init() tea.Cmd {
defer func() { defer func() {
if isLocalZipFile() { if isLocalZipFile() {
go installLocalFontZIP(m.font) go installLocalFontZIP(m.font, m.system)
return return
} }
go getFontsList() go getFontsList()
@ -215,7 +209,7 @@ func (m *main) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case zipMsg: case zipMsg:
m.state = installFont m.state = installFont
defer func() { defer func() {
go installFontZIP(msg) go installFontZIP(msg, m.system)
}() }()
m.spinner.Spinner = spinner.Dot m.spinner.Spinner = spinner.Dot
return m, m.spinner.Tick return m, m.spinner.Tick
@ -250,8 +244,6 @@ func (m *main) View() string {
return "\n" + m.list.View() return "\n" + m.list.View()
case downloadFont: case downloadFont:
return textStyle.Render(fmt.Sprintf("%s Downloading %s", m.spinner.View(), m.font)) return textStyle.Render(fmt.Sprintf("%s Downloading %s", m.spinner.View(), m.font))
case admin:
return textStyle.Render("You need to be admin to install a font on Windows")
case unzipFont: 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.font))
case installFont: case installFont:
@ -264,10 +256,10 @@ func (m *main) View() string {
return "" return ""
} }
func Run(font string, needsAdmin bool) { func Run(font string, system bool) {
main := &main{ main := &main{
font: font, font: font,
needsAdmin: needsAdmin, system: system,
} }
program = tea.NewProgram(main) program = tea.NewProgram(main)
if _, err := program.Run(); err != nil { if _, err := program.Run(); err != nil {

View file

@ -10,7 +10,7 @@ import (
"strings" "strings"
) )
func InstallZIP(data []byte) (err error) { func InstallZIP(data []byte, user bool) (err error) {
bytesReader := bytes.NewReader(data) bytesReader := bytes.NewReader(data)
zipReader, err := zip.NewReader(bytesReader, int64(bytesReader.Len())) zipReader, err := zip.NewReader(bytesReader, int64(bytesReader.Len()))
@ -50,7 +50,7 @@ func InstallZIP(data []byte) (err error) {
} }
for _, font := range fonts { for _, font := range fonts {
if err = install(font); err != nil { if err = install(font, user); err != nil {
return err return err
} }
} }

View file

@ -9,7 +9,7 @@ import (
var FontsDir = path.Join(os.Getenv("HOME"), "Library", "Fonts") var FontsDir = path.Join(os.Getenv("HOME"), "Library", "Fonts")
func install(font *Font) (err error) { func install(font *Font, _ bool) (err error) {
// On darwin/OSX, the user's fonts directory is ~/Library/Fonts, // On darwin/OSX, the user's fonts directory is ~/Library/Fonts,
// and fonts should be installed directly into that path; // and fonts should be installed directly into that path;
// i.e., not in subfolders. // i.e., not in subfolders.

View file

@ -14,7 +14,7 @@ import (
// FontsDir denotes the path to the user's fonts directory on Unix-like systems. // FontsDir denotes the path to the user's fonts directory on Unix-like systems.
var FontsDir = path.Join(os.Getenv("HOME"), "/.local/share/fonts") var FontsDir = path.Join(os.Getenv("HOME"), "/.local/share/fonts")
func install(font *Font) (err error) { func install(font *Font, _ bool) (err error) {
// On Linux, fontconfig can understand subdirectories. So, to keep the // On Linux, fontconfig can understand subdirectories. So, to keep the
// font directory clean, install all font files for a particular font // font directory clean, install all font files for a particular font
// family into a subdirectory named after the family (with hyphens instead // family into a subdirectory named after the family (with hyphens instead

View file

@ -12,34 +12,49 @@ import (
) )
// https://docs.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-addfontresourcea // https://docs.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-addfontresourcea
var FontsDir = filepath.Join(os.Getenv("WINDIR"), "Fonts")
const ( const (
WM_FONTCHANGE = 0x001D //nolint:revive WM_FONTCHANGE = 0x001D //nolint:revive
HWND_BROADCAST = 0xFFFF //nolint:revive HWND_BROADCAST = 0xFFFF //nolint:revive
) )
func install(font *Font) (err error) { func install(font *Font, admin bool) (err error) {
// To install a font on Windows: // To install a font on Windows:
// - Copy the file to the fonts directory // - Copy the file to the fonts directory
// - Add registry entry // - Add registry entry
// - Call AddFontResourceW to set the font // - Call AddFontResourceW to set the font
// - Notify other applications that the fonts have changed // - Notify other applications that the fonts have changed
fullPath := filepath.Join(FontsDir, font.FileName) fontsDir := filepath.Join(os.Getenv("WINDIR"), "Fonts")
if !admin {
fontsDir = filepath.Join(os.Getenv("USERPROFILE"), "AppData", "Local", "Microsoft", "Windows", "Fonts")
}
fullPath := filepath.Join(fontsDir, font.FileName)
// validate if font is already installed, remove it in case it is
if _, err := os.Stat(fullPath); err == nil {
if err = os.Remove(fullPath); err != nil {
return fmt.Errorf("Unable to remove existing font file: %s", err.Error())
}
}
err = os.WriteFile(fullPath, font.Data, 0644) err = os.WriteFile(fullPath, font.Data, 0644)
if err != nil { if err != nil {
return return fmt.Errorf("Unable to write font file: %s", err.Error())
} }
// Add registry entry // Add registry entry
k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts`, registry.WRITE) reg := registry.LOCAL_MACHINE
if !admin {
reg = registry.CURRENT_USER
}
k, err := registry.OpenKey(reg, `SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts`, registry.WRITE)
if err != nil { if err != nil {
// If this fails, remove the font file as well. // If this fails, remove the font file as well.
if nexterr := os.Remove(fullPath); nexterr != nil { if nexterr := os.Remove(fullPath); nexterr != nil {
return nexterr return errors.New("Unable to delete font file after registry key open error")
} }
return err return fmt.Errorf("Unable to open registry key: %s", err.Error())
} }
defer k.Close() defer k.Close()
@ -47,10 +62,10 @@ func install(font *Font) (err error) {
if err = k.SetStringValue(name, font.FileName); err != nil { if err = k.SetStringValue(name, font.FileName); err != nil {
// If this fails, remove the font file as well. // If this fails, remove the font file as well.
if nexterr := os.Remove(fullPath); nexterr != nil { if nexterr := os.Remove(fullPath); nexterr != nil {
return nexterr return errors.New("Unable to delete font file after registry key set error")
} }
return err return fmt.Errorf("Unable to set registry value: %s", err.Error())
} }
gdi32 := syscall.NewLazyDLL("gdi32.dll") gdi32 := syscall.NewLazyDLL("gdi32.dll")
@ -63,7 +78,7 @@ func install(font *Font) (err error) {
ret, _, _ := proc.Call(uintptr(unsafe.Pointer(fontPtr))) ret, _, _ := proc.Call(uintptr(unsafe.Pointer(fontPtr)))
if ret == 0 { if ret == 0 {
return errors.New("unable to add font resource") return errors.New("Unable to add font resource using AddFontResourceW")
} }
return nil return nil

View file

@ -28,7 +28,9 @@ To see the icons displayed in Oh My Posh, **install** a [Nerd Font][nerdfonts],
Oh My Posh has a CLI to help you select and install a [Nerd Font][nerdfonts] (beta): Oh My Posh has a CLI to help you select and install a [Nerd Font][nerdfonts] (beta):
:::caution Windows :::caution Windows
This command needs to be executed as admin. The fonts are installed system wide. This command is best executed as admin so that fonts are installed system wide.
In case you have no admin rights, you can install the fonts by adding the `--user` flag.
Do know this can have side-effects when using certain applications.
::: :::
```bash ```bash