diff --git a/src/cli/font.go b/src/cli/font.go index 32197a78..62cd247b 100644 --- a/src/cli/font.go +++ b/src/cli/font.go @@ -10,6 +10,8 @@ import ( ) var ( + user bool + // fontCmd can work with fonts fontCmd = &cobra.Command{ 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.Init() 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 case "configure": 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 RootCmd.AddCommand(fontCmd) + fontCmd.Flags().BoolVar(&user, "user", false, "install font as user") } diff --git a/src/font/cli.go b/src/font/cli.go index c62fcf63..18e1a683 100644 --- a/src/font/cli.go +++ b/src/font/cli.go @@ -64,18 +64,17 @@ const ( downloadFont unzipFont installFont - admin quit done ) type main struct { - spinner spinner.Model - list *list.Model - needsAdmin bool - font string - state state - err error + spinner spinner.Model + list *list.Model + system bool + font string + state state + err error } func (m *main) buildFontList(nerdFonts []*Asset) { @@ -115,17 +114,17 @@ func downloadFontZip(location string) { program.Send(zipMsg(zipFile)) } -func installLocalFontZIP(zipFile string) { +func installLocalFontZIP(zipFile string, user bool) { data, err := os.ReadFile(zipFile) if err != nil { program.Send(errMsg(err)) return } - installFontZIP(data) + installFontZIP(data, user) } -func installFontZIP(zipFile []byte) { - err := InstallZIP(zipFile) +func installFontZIP(zipFile []byte, user bool) { + err := InstallZIP(zipFile, user) if err != nil { program.Send(errMsg(err)) return @@ -134,11 +133,6 @@ func installFontZIP(zipFile []byte) { } func (m *main) Init() tea.Cmd { - if m.needsAdmin { - m.state = admin - return tea.Quit - } - isLocalZipFile := func() bool { return !strings.HasPrefix(m.font, "https") && strings.HasSuffix(m.font, ".zip") } @@ -157,7 +151,7 @@ func (m *main) Init() tea.Cmd { defer func() { if isLocalZipFile() { - go installLocalFontZIP(m.font) + go installLocalFontZIP(m.font, m.system) return } go getFontsList() @@ -215,7 +209,7 @@ func (m *main) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case zipMsg: m.state = installFont defer func() { - go installFontZIP(msg) + go installFontZIP(msg, m.system) }() m.spinner.Spinner = spinner.Dot return m, m.spinner.Tick @@ -250,8 +244,6 @@ func (m *main) View() string { return "\n" + m.list.View() case downloadFont: 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: return textStyle.Render(fmt.Sprintf("%s Extracting %s", m.spinner.View(), m.font)) case installFont: @@ -264,10 +256,10 @@ func (m *main) View() string { return "" } -func Run(font string, needsAdmin bool) { +func Run(font string, system bool) { main := &main{ - font: font, - needsAdmin: needsAdmin, + font: font, + system: system, } program = tea.NewProgram(main) if _, err := program.Run(); err != nil { diff --git a/src/font/install.go b/src/font/install.go index 0789ce09..864223c6 100644 --- a/src/font/install.go +++ b/src/font/install.go @@ -10,7 +10,7 @@ import ( "strings" ) -func InstallZIP(data []byte) (err error) { +func InstallZIP(data []byte, user bool) (err error) { bytesReader := bytes.NewReader(data) zipReader, err := zip.NewReader(bytesReader, int64(bytesReader.Len())) @@ -50,7 +50,7 @@ func InstallZIP(data []byte) (err error) { } for _, font := range fonts { - if err = install(font); err != nil { + if err = install(font, user); err != nil { return err } } diff --git a/src/font/install_darwin.go b/src/font/install_darwin.go index 437654ef..d99c3df7 100644 --- a/src/font/install_darwin.go +++ b/src/font/install_darwin.go @@ -9,7 +9,7 @@ import ( 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, // and fonts should be installed directly into that path; // i.e., not in subfolders. diff --git a/src/font/install_unix.go b/src/font/install_unix.go index 0c978b09..700277f0 100644 --- a/src/font/install_unix.go +++ b/src/font/install_unix.go @@ -14,7 +14,7 @@ import ( // FontsDir denotes the path to the user's fonts directory on Unix-like systems. 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 // font directory clean, install all font files for a particular font // family into a subdirectory named after the family (with hyphens instead diff --git a/src/font/install_windows.go b/src/font/install_windows.go index c7b2939a..5685d3eb 100644 --- a/src/font/install_windows.go +++ b/src/font/install_windows.go @@ -12,34 +12,49 @@ import ( ) // https://docs.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-addfontresourcea -var FontsDir = filepath.Join(os.Getenv("WINDIR"), "Fonts") const ( WM_FONTCHANGE = 0x001D //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: // - Copy the file to the fonts directory // - Add registry entry // - Call AddFontResourceW to set the font // - 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) if err != nil { - return + return fmt.Errorf("Unable to write font file: %s", err.Error()) } // 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 this fails, remove the font file as well. 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() @@ -47,10 +62,10 @@ func install(font *Font) (err error) { if err = k.SetStringValue(name, font.FileName); err != nil { // If this fails, remove the font file as well. 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") @@ -63,7 +78,7 @@ func install(font *Font) (err error) { ret, _, _ := proc.Call(uintptr(unsafe.Pointer(fontPtr))) if ret == 0 { - return errors.New("unable to add font resource") + return errors.New("Unable to add font resource using AddFontResourceW") } return nil diff --git a/website/docs/installation/fonts.mdx b/website/docs/installation/fonts.mdx index 514ee023..603cd00d 100644 --- a/website/docs/installation/fonts.mdx +++ b/website/docs/installation/fonts.mdx @@ -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): :::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