From fece104e7391b554ae82b7ed596da31c1d24c7b0 Mon Sep 17 00:00:00 2001 From: Jan De Dobbeleer Date: Thu, 15 Feb 2024 14:18:53 +0100 Subject: [PATCH] fix(image): use fixed image width --- src/cli/config_export_image.go | 30 ++++----- src/engine/image.go | 118 +++++++++++++++++++-------------- website/export_themes.js | 66 +++++------------- 3 files changed, 97 insertions(+), 117 deletions(-) diff --git a/src/cli/config_export_image.go b/src/cli/config_export_image.go index 54925df7..67551b28 100644 --- a/src/cli/config_export_image.go +++ b/src/cli/config_export_image.go @@ -12,11 +12,11 @@ import ( ) var ( - author string - cursorPadding int - rPromptOffset int - bgColor string - outputImage string + author string + // cursorPadding int + // rPromptOffset int + bgColor string + outputImage string ) // imageCmd represents the image command @@ -49,10 +49,12 @@ Exports the config to an image file using customized output options.`, Run: func(_ *cobra.Command, _ []string) { env := &platform.Shell{ CmdFlags: &platform.Flags{ - Config: config, - Shell: shell.GENERIC, + Config: config, + Shell: shell.GENERIC, + TerminalWidth: 150, }, } + env.Init() defer env.Close() cfg := engine.LoadConfig(env) @@ -80,12 +82,10 @@ Exports the config to an image file using customized output options.`, prompt := eng.Primary() imageCreator := &engine.ImageRenderer{ - AnsiString: prompt, - Author: author, - CursorPadding: cursorPadding, - RPromptOffset: rPromptOffset, - BgColor: bgColor, - Ansi: writer, + AnsiString: prompt, + Author: author, + BgColor: bgColor, + Ansi: writer, } if outputImage != "" { @@ -108,8 +108,8 @@ Exports the config to an image file using customized output options.`, func init() { //nolint:gochecknoinits imageCmd.Flags().StringVar(&author, "author", "", "config author") imageCmd.Flags().StringVar(&bgColor, "background-color", "", "image background color") - imageCmd.Flags().IntVar(&cursorPadding, "cursor-padding", 0, "prompt cursor padding") - imageCmd.Flags().IntVar(&rPromptOffset, "rprompt-offset", 0, "right prompt offset") + // imageCmd.Flags().IntVar(&cursorPadding, "cursor-padding", 0, "prompt cursor padding") + // imageCmd.Flags().IntVar(&rPromptOffset, "rprompt-offset", 0, "right prompt offset") imageCmd.Flags().StringVarP(&outputImage, "output", "o", "", "image file (.png) to export to") exportCmd.AddCommand(imageCmd) } diff --git a/src/engine/image.go b/src/engine/image.go index bc596d4d..70d41657 100644 --- a/src/engine/image.go +++ b/src/engine/image.go @@ -339,6 +339,73 @@ func (ir *ImageRenderer) runeAdditionalWidth(r rune) int { return 0 } +func (ir *ImageRenderer) cleanContent() { + // clean abundance of empty lines + ir.AnsiString = strings.Trim(ir.AnsiString, "\n") + ir.AnsiString = "\n" + ir.AnsiString + + // clean string before render + ir.AnsiString = strings.ReplaceAll(ir.AnsiString, "\x1b[m", "\x1b[0m") + ir.AnsiString = strings.ReplaceAll(ir.AnsiString, "\x1b[K", "") + ir.AnsiString = strings.ReplaceAll(ir.AnsiString, "\x1b[0J", "") + ir.AnsiString = strings.ReplaceAll(ir.AnsiString, "\x1b[27m", "") + ir.AnsiString = strings.ReplaceAll(ir.AnsiString, "\x1b8", "") + ir.AnsiString = strings.ReplaceAll(ir.AnsiString, "\u2800", " ") + + // cursor indication + saveCursorAnsi := "\x1b7" + if !strings.Contains(ir.AnsiString, saveCursorAnsi) { + ir.AnsiString += "_" + } + ir.AnsiString = strings.ReplaceAll(ir.AnsiString, saveCursorAnsi, "_") + + // replace rprompt with adding and mark right aligned blocks with a pointer + ir.AnsiString = strings.ReplaceAll(ir.AnsiString, "\x1b[1000C", strings.Repeat(" ", ir.RPromptOffset)) + + // add watermarks + ir.AnsiString += "\n\n\x1b[1mohmyposh.dev\x1b[22m" + if len(ir.Author) > 0 { + createdBy := fmt.Sprintf(" by \x1b[1m%s\x1b[22m", ir.Author) + ir.AnsiString += createdBy + } +} + +func (ir *ImageRenderer) measureContent() (width, height float64) { + linewidth := 145 + linewidth += ir.additionalWidth() + + tmpDrawer := &font.Drawer{Face: ir.regular} + advance := tmpDrawer.MeasureString(strings.Repeat(" ", linewidth)) + width = float64(advance >> 6) + // height, lines times font height and line spacing + height = float64(len(strings.Split(ir.AnsiString, "\n"))) * ir.fontHeight() * ir.lineSpacing + return width, height +} + +/* +additionalWidth returns the number of additional characters of width to allocate when drawing +for characters that are 2 wide. A standard character will return 0 +Nerd Font glyphs will return 1, since most are double width +*/ +func (ir *ImageRenderer) additionalWidth() int { + longest := 0 + var longestLine string + for _, line := range strings.Split(ir.AnsiString, "\n") { + length := ir.lenWithoutANSI(line) + if length > longest { + longestLine = line + longest = length + } + } + + var additionalWidth int + for _, rune := range longestLine { + additionalWidth += ir.runeAdditionalWidth(rune) + } + + return additionalWidth +} + func (ir *ImageRenderer) lenWithoutANSI(text string) int { if len(text) == 0 { return 0 @@ -363,57 +430,6 @@ func (ir *ImageRenderer) lenWithoutANSI(text string) int { return length } -func (ir *ImageRenderer) calculateWidth() int { - longest := 0 - for _, line := range strings.Split(ir.AnsiString, "\n") { - length := ir.lenWithoutANSI(line) - if length > longest { - longest = length - } - } - return longest -} - -func (ir *ImageRenderer) cleanContent() { - rPromptAnsi := "\x1b7\x1b[1000C" - hasRPrompt := strings.Contains(ir.AnsiString, rPromptAnsi) - // clean abundance of empty lines - ir.AnsiString = strings.Trim(ir.AnsiString, "\n") - ir.AnsiString = "\n" + ir.AnsiString - // clean string before render - ir.AnsiString = strings.ReplaceAll(ir.AnsiString, "\x1b[m", "\x1b[0m") - ir.AnsiString = strings.ReplaceAll(ir.AnsiString, "\x1b[K", "") - ir.AnsiString = strings.ReplaceAll(ir.AnsiString, "\x1b[0J", "") - ir.AnsiString = strings.ReplaceAll(ir.AnsiString, "\x1b[27m", "") - ir.AnsiString = strings.ReplaceAll(ir.AnsiString, "\x1b[1F", "") - ir.AnsiString = strings.ReplaceAll(ir.AnsiString, "\x1b8", "") - ir.AnsiString = strings.ReplaceAll(ir.AnsiString, "\u2800", " ") - // replace rprompt with adding and mark right aligned blocks with a pointer - ir.AnsiString = strings.ReplaceAll(ir.AnsiString, rPromptAnsi, fmt.Sprintf("_%s", strings.Repeat(" ", ir.CursorPadding))) - ir.AnsiString = strings.ReplaceAll(ir.AnsiString, "\x1b[1000C", strings.Repeat(" ", ir.RPromptOffset)) - if !hasRPrompt { - ir.AnsiString += fmt.Sprintf("_%s", strings.Repeat(" ", ir.CursorPadding)) - } - // add watermarks - ir.AnsiString += "\n\n\x1b[1mohmyposh.dev\x1b[22m" - if len(ir.Author) > 0 { - createdBy := fmt.Sprintf(" by \x1b[1m%s\x1b[22m", ir.Author) - ir.AnsiString += createdBy - } -} - -func (ir *ImageRenderer) measureContent() (width, height float64) { - // get the longest line - linewidth := ir.calculateWidth() - // width, taken from the longest line - tmpDrawer := &font.Drawer{Face: ir.regular} - advance := tmpDrawer.MeasureString(strings.Repeat(" ", linewidth)) - width = float64(advance >> 6) - // height, lines times font height and line spacing - height = float64(len(strings.Split(ir.AnsiString, "\n"))) * ir.fontHeight() * ir.lineSpacing - return width, height -} - func (ir *ImageRenderer) SavePNG() error { var f = func(value float64) float64 { return ir.factor * value } diff --git a/website/export_themes.js b/website/export_themes.js index f6d731d7..f5c3d3bc 100644 --- a/website/export_themes.js +++ b/website/export_themes.js @@ -8,10 +8,8 @@ const exec = util.promisify(require('child_process').exec); const themesConfigDir = "./../themes"; const themesStaticDir = "./static/img/themes"; -function newThemeConfig(rpromptOffset = 40, cursorPadding = 30, author = "", bgColor = "#151515") { +function newThemeConfig(author = "", bgColor = "#151515") { var config = { - rpromptOffset: rpromptOffset, - cursorPadding: cursorPadding, author: author, bgColor: bgColor }; @@ -23,51 +21,18 @@ function isValidTheme(theme) { } let themeConfigOverrrides = new Map(); -themeConfigOverrrides.set('agnoster.omp.json', newThemeConfig(40, 40)); -themeConfigOverrrides.set('agnosterplus.omp.json', newThemeConfig(80)); -themeConfigOverrrides.set('amro.omp.json', newThemeConfig(40, 100, 'AmRo', '#1C2029')); -themeConfigOverrrides.set('avit.omp.json', newThemeConfig(40, 80)); -themeConfigOverrrides.set('blueish.omp.json', newThemeConfig(40, 100)); -themeConfigOverrrides.set('cert.omp.json', newThemeConfig(40, 50)); -themeConfigOverrrides.set('chips.omp.json', newThemeConfig(25, 30, 'CodexLink | v1.2.4, Single Width (07/11/2023) | https://github.com/CodexLink/chips.omp.json')); -themeConfigOverrrides.set('cinnamon.omp.json', newThemeConfig(40, 80)); -themeConfigOverrrides.set('craver.omp.json', newThemeConfig(40, 80, 'Nick Craver', '#282c34')); -themeConfigOverrrides.set('darkblood.omp.json', newThemeConfig(40, 40)); -themeConfigOverrrides.set('honukai.omp.json', newThemeConfig(20)); -themeConfigOverrrides.set('hotstick.minimal.omp.json', newThemeConfig(40, 10)); -themeConfigOverrrides.set('hunk.omp.json', newThemeConfig(40, 15, 'Paris Qian')); -themeConfigOverrrides.set('huvix.omp.json', newThemeConfig(40, 70)); -themeConfigOverrrides.set('jandedobbeleer.omp.json', newThemeConfig(40, 15)); -themeConfigOverrrides.set('kushal.omp.json', newThemeConfig(90, 30, 'Kushal-Chandar')); -themeConfigOverrrides.set('lambda.omp.json', newThemeConfig(40, 40)); -themeConfigOverrrides.set('marcduiker.omp.json', newThemeConfig(0, 40)); -themeConfigOverrrides.set('material.omp.json', newThemeConfig(40, 40)); -themeConfigOverrrides.set('microverse-power.omp.json', newThemeConfig(40, 100)); -themeConfigOverrrides.set('negligible.omp.json', newThemeConfig(10)); -themeConfigOverrrides.set('night-owl.omp.json', newThemeConfig(40, 0, 'Mr-Vipi', '#011627')); -themeConfigOverrrides.set('paradox.omp.json', newThemeConfig(40, 100)); -themeConfigOverrrides.set('powerlevel10k_classic.omp.json', newThemeConfig(10)); -themeConfigOverrrides.set('powerlevel10k_lean.omp.json', newThemeConfig(80)); -themeConfigOverrrides.set('powerline.omp.json', newThemeConfig(40, 40)); -themeConfigOverrrides.set('pure.omp.json', newThemeConfig(40, 80)); -themeConfigOverrrides.set('quick-term.omp.json', newThemeConfig(15, 0, 'SokLay')) -themeConfigOverrrides.set('remk.omp.json', newThemeConfig(40, 40)); -themeConfigOverrrides.set('robbyrussell.omp.json', newThemeConfig(40, 40)); -themeConfigOverrrides.set('slim.omp.json', newThemeConfig(10, 80)); -themeConfigOverrrides.set('slimfat.omp.json', newThemeConfig(10, 93)); -themeConfigOverrrides.set('space.omp.json', newThemeConfig(40, 40)); -themeConfigOverrrides.set('spaceship.omp.json', newThemeConfig(40, 40)); -themeConfigOverrrides.set('star.omp.json', newThemeConfig(40, 70)); -themeConfigOverrrides.set('stelbent.minimal.omp.json', newThemeConfig(70)); -themeConfigOverrrides.set('tonybaloney.omp.json', newThemeConfig(0, 40)); -themeConfigOverrrides.set('unicorn.omp.json', newThemeConfig(0, 40)); -themeConfigOverrrides.set('ys.omp.json', newThemeConfig(40, 100)); -themeConfigOverrrides.set('zash.omp.json', newThemeConfig(40, 40)); -themeConfigOverrrides.set('catppuccin.omp.json', newThemeConfig(40, 40, 'IrwinJuice', '#24273A')); -themeConfigOverrrides.set('catppuccin_latte.omp.json', newThemeConfig(40, 40, 'IrwinJuice', '#EFF1F5')); -themeConfigOverrrides.set('catppuccin_frappe.omp.json', newThemeConfig(40, 40, 'IrwinJuice', '#303446')); -themeConfigOverrrides.set('catppuccin_macchiato.omp.json', newThemeConfig(40, 40, 'IrwinJuice', '#24273A')); -themeConfigOverrrides.set('catppuccin_mocha.omp.json', newThemeConfig(40, 40, 'IrwinJuice', '#1E1E2E')); +themeConfigOverrrides.set('amro.omp.json', newThemeConfig('AmRo', '#1C2029')); +themeConfigOverrrides.set('chips.omp.json', newThemeConfig('CodexLink | v1.2.4, Single Width (07/11/2023) | https://github.com/CodexLink/chips.omp.json')); +themeConfigOverrrides.set('craver.omp.json', newThemeConfig('Nick Craver', '#282c34')); +themeConfigOverrrides.set('hunk.omp.json', newThemeConfig('Paris Qian')); +themeConfigOverrrides.set('kushal.omp.json', newThemeConfig('Kushal-Chandar')); +themeConfigOverrrides.set('night-owl.omp.json', newThemeConfig('Mr-Vipi', '#011627')); +themeConfigOverrrides.set('quick-term.omp.json', newThemeConfig('SokLay')) +themeConfigOverrrides.set('catppuccin.omp.json', newThemeConfig('IrwinJuice', '#24273A')); +themeConfigOverrrides.set('catppuccin_latte.omp.json', newThemeConfig('IrwinJuice', '#EFF1F5')); +themeConfigOverrrides.set('catppuccin_frappe.omp.json', newThemeConfig('IrwinJuice', '#303446')); +themeConfigOverrrides.set('catppuccin_macchiato.omp.json', newThemeConfig('IrwinJuice', '#24273A')); +themeConfigOverrrides.set('catppuccin_mocha.omp.json', newThemeConfig('IrwinJuice', '#1E1E2E')); (async () => { const themes = await fs.promises.readdir(themesConfigDir); @@ -85,10 +50,7 @@ themeConfigOverrrides.set('catppuccin_mocha.omp.json', newThemeConfig(40, 40, 'I } let poshCommand = `oh-my-posh config export image --config=${configPath}`; - poshCommand += ` --rprompt-offset=${config.rpromptOffset}`; - poshCommand += ` --cursor-padding=${config.cursorPadding}`; poshCommand += ` --background-color=${config.bgColor}`; - poshCommand += ` --terminal-width=200`; if (config.author !== '') { poshCommand += ` --author="${config.author}"`; } @@ -100,6 +62,8 @@ themeConfigOverrrides.set('catppuccin_mocha.omp.json', newThemeConfig(40, 40, 'I continue; } + console.info(`Exported ${theme}`); + const themeName = theme.slice(0, -9); const image = themeName + '.png'; const toPath = path.join(themesStaticDir, image);