Repository: JanDeDobbeleer/oh-my-posh Branch: main Commit: 2675e61b2bc2 Files: 874 Total size: 3.2 MB Directory structure: gitextract_dbo6fjze/ ├── .all-contributorsrc ├── .commitlintrc.yml ├── .config/ │ └── configuration.winget ├── .devcontainer/ │ ├── Dockerfile │ ├── Microsoft.PowerShell_profile.ps1 │ ├── config.fish │ └── devcontainer.json ├── .editorconfig ├── .gitattributes ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug.yml │ │ ├── config.yml │ │ ├── docs.yml │ │ ├── enhancement.yml │ │ └── feat.yml │ ├── PULL_REQUEST_TEMPLATE.md │ ├── agents/ │ │ └── architecture.md │ ├── copilot-instructions.md │ ├── dependabot.yml │ ├── holopin.yml │ ├── stale.yml │ └── workflows/ │ ├── ai-changelog.yml │ ├── android.yml │ ├── bluesky.yml │ ├── build_code.yml │ ├── close_themes_pr.yml │ ├── code.yml │ ├── commits.yml │ ├── composite/ │ │ └── bootstrap-go/ │ │ └── action.yml │ ├── contributors.yml │ ├── copilot-setup-steps.yml │ ├── delete_store_submission.yml │ ├── dependabot.yml │ ├── discord.yml │ ├── docs.yml │ ├── edit_rights.yml │ ├── gomod.yml │ ├── homebrew.yml │ ├── lock.yml │ ├── markdown.yml │ ├── merge_contributions_pr.yml │ ├── microsoft_store.yml │ ├── publish-mcp.yml │ ├── release.yml │ └── vale.yml ├── .gitignore ├── .markdownlint-cli2.yaml ├── .prettierrc ├── .vale.ini ├── .versionrc.json ├── .vscode/ │ ├── extensions.json │ ├── launch.json │ ├── settings.json │ └── tasks.json ├── AGENTS.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── COPYING ├── README.md ├── SECURITY.md ├── apm.lock.yaml ├── apm.yml ├── build/ │ ├── post.ps1 │ └── pre.ps1 ├── packages/ │ └── msi/ │ ├── README.md │ ├── appxmanifest.xml │ ├── build.ps1 │ ├── dsc/ │ │ ├── oh-my-posh.config.dsc.resource.json │ │ ├── oh-my-posh.font.dsc.resource.json │ │ └── oh-my-posh.shell.dsc.resource.json │ ├── mapping.txt │ └── oh-my-posh.wxs ├── src/ │ ├── .golangci.yml │ ├── .goreleaser.yml │ ├── build/ │ │ └── version.go │ ├── cache/ │ │ ├── cache.go │ │ ├── clear.go │ │ ├── command.go │ │ ├── duration.go │ │ ├── duration_test.go │ │ ├── file_map_windows.go │ │ ├── file_unix.go │ │ ├── file_windows.go │ │ ├── init.go │ │ ├── path.go │ │ ├── path_unix.go │ │ ├── path_windows.go │ │ ├── store.go │ │ ├── store_test.go │ │ └── template.go │ ├── cli/ │ │ ├── args.go │ │ ├── auth/ │ │ │ ├── cli.go │ │ │ ├── copilot.go │ │ │ ├── ytmda.go │ │ │ └── ytmda_test.go │ │ ├── auth.go │ │ ├── cache.go │ │ ├── claude.go │ │ ├── config.go │ │ ├── config_export.go │ │ ├── config_export_image.go │ │ ├── debug.go │ │ ├── disable.go │ │ ├── edit.go │ │ ├── enable.go │ │ ├── font/ │ │ │ ├── download.go │ │ │ ├── dsc.go │ │ │ ├── font.go │ │ │ ├── fonts.go │ │ │ ├── install.go │ │ │ ├── install_darwin.go │ │ │ ├── install_unix.go │ │ │ ├── install_windows.go │ │ │ └── tui.go │ │ ├── font.go │ │ ├── get.go │ │ ├── image/ │ │ │ ├── config.go │ │ │ ├── config_test.go │ │ │ ├── fonts.go │ │ │ ├── image.go │ │ │ └── image_test.go │ │ ├── init.go │ │ ├── notice.go │ │ ├── print.go │ │ ├── progress/ │ │ │ ├── model.go │ │ │ └── reader.go │ │ ├── root.go │ │ ├── shell.go │ │ ├── stream.go │ │ ├── stream_test.go │ │ ├── toggle.go │ │ ├── upgrade/ │ │ │ ├── config.go │ │ │ ├── install.go │ │ │ ├── install_noop.go │ │ │ ├── install_windows.go │ │ │ ├── notice.go │ │ │ ├── notice_test.go │ │ │ ├── public_key.pem │ │ │ ├── tui.go │ │ │ ├── tui_test.go │ │ │ ├── verify.go │ │ │ └── verify_test.go │ │ ├── upgrade.go │ │ └── version.go │ ├── color/ │ │ ├── colors.go │ │ ├── colors_darwin.go │ │ ├── colors_test.go │ │ ├── colors_unix.go │ │ ├── colors_windows.go │ │ ├── cycle.go │ │ ├── keywords.go │ │ ├── palette.go │ │ ├── palette_test.go │ │ └── palettes.go │ ├── config/ │ │ ├── backup.go │ │ ├── block.go │ │ ├── cache.go │ │ ├── config.go │ │ ├── config_test.go │ │ ├── default.go │ │ ├── dsc.go │ │ ├── gob.go │ │ ├── load.go │ │ ├── merge.go │ │ ├── merge_test.go │ │ ├── migrate_glyphs.go │ │ ├── migrate_glyphs_test.go │ │ ├── responsive.go │ │ ├── responsive_test.go │ │ ├── segment.go │ │ ├── segment_test.go │ │ └── segment_types.go │ ├── constants/ │ │ ├── constants_unix.go │ │ └── constants_windows.go │ ├── dsc/ │ │ ├── cli.go │ │ ├── error.go │ │ └── resource.go │ ├── generics/ │ │ ├── convert.go │ │ ├── pool.go │ │ └── slices.go │ ├── go.mod │ ├── go.sum │ ├── log/ │ │ ├── log.go │ │ └── print.go │ ├── main.go │ ├── main_test.go │ ├── maps/ │ │ ├── concurrent.go │ │ ├── config.go │ │ └── simple.go │ ├── metadata.json │ ├── prompt/ │ │ ├── debug.go │ │ ├── engine.go │ │ ├── engine_test.go │ │ ├── extra.go │ │ ├── preview.go │ │ ├── primary.go │ │ ├── rprompt.go │ │ ├── segments.go │ │ ├── segments_test.go │ │ ├── status.go │ │ ├── streaming.go │ │ ├── streaming_test.go │ │ └── tooltip.go │ ├── regex/ │ │ ├── regex.go │ │ └── regex_test.go │ ├── runtime/ │ │ ├── battery/ │ │ │ ├── battery.go │ │ │ ├── battery_darwin.go │ │ │ ├── battery_darwin_test.go │ │ │ ├── battery_linux.go │ │ │ ├── battery_netbsd.go │ │ │ ├── battery_openandfreebsd.go │ │ │ ├── battery_openandfreebsd_test.go │ │ │ ├── battery_windows.go │ │ │ ├── battery_windows_nix.go │ │ │ ├── battery_windows_nix_test.go │ │ │ ├── errors.go │ │ │ └── errors_test.go │ │ ├── cmd/ │ │ │ ├── run.go │ │ │ └── run_test.go │ │ ├── environment.go │ │ ├── http/ │ │ │ ├── connection.go │ │ │ ├── download.go │ │ │ ├── http.go │ │ │ ├── oauth.go │ │ │ ├── oauth_test.go │ │ │ ├── request.go │ │ │ └── request_test.go │ │ ├── jobs/ │ │ │ ├── jobs_common.go │ │ │ ├── jobs_other.go │ │ │ └── jobs_windows.go │ │ ├── mock/ │ │ │ └── environment.go │ │ ├── networks_windows.go │ │ ├── path/ │ │ │ ├── clean.go │ │ │ ├── home.go │ │ │ └── separator.go │ │ ├── terminal.go │ │ ├── terminal_darwin.go │ │ ├── terminal_test.go │ │ ├── terminal_unix.go │ │ ├── terminal_unix_test.go │ │ ├── terminal_windows.go │ │ ├── terminal_windows_nix.go │ │ └── win32_windows.go │ ├── segments/ │ │ ├── angular.go │ │ ├── argocd.go │ │ ├── argocd_test.go │ │ ├── aurelia.go │ │ ├── aws.go │ │ ├── aws_test.go │ │ ├── az.go │ │ ├── az_functions.go │ │ ├── az_test.go │ │ ├── azd.go │ │ ├── azd_test.go │ │ ├── base.go │ │ ├── battery.go │ │ ├── bazel.go │ │ ├── bazel_test.go │ │ ├── brewfather.go │ │ ├── brewfather_test.go │ │ ├── buf.go │ │ ├── buf_test.go │ │ ├── bun.go │ │ ├── bun_test.go │ │ ├── carbon_intensity.go │ │ ├── carbon_intensity_test.go │ │ ├── cds.go │ │ ├── cds_test.go │ │ ├── cf.go │ │ ├── cf_target.go │ │ ├── cf_target_test.go │ │ ├── cf_test.go │ │ ├── claude.go │ │ ├── claude_test.go │ │ ├── clojure.go │ │ ├── clojure_test.go │ │ ├── cmake.go │ │ ├── cmake_test.go │ │ ├── connection.go │ │ ├── connection_test.go │ │ ├── copilot.go │ │ ├── copilot_test.go │ │ ├── crystal.go │ │ ├── crystal_test.go │ │ ├── dart.go │ │ ├── dart_test.go │ │ ├── deno.go │ │ ├── deno_test.go │ │ ├── docker.go │ │ ├── docker_test.go │ │ ├── dotnet.go │ │ ├── dotnet_test.go │ │ ├── elixir.go │ │ ├── elixir_test.go │ │ ├── executiontime.go │ │ ├── executiontime_test.go │ │ ├── firebase.go │ │ ├── firebase_test.go │ │ ├── flutter.go │ │ ├── flutter_test.go │ │ ├── fortran.go │ │ ├── fortran_test.go │ │ ├── fossil.go │ │ ├── fossil_test.go │ │ ├── gcp.go │ │ ├── gcp_test.go │ │ ├── git.go │ │ ├── git_test.go │ │ ├── git_unix.go │ │ ├── git_unix_test.go │ │ ├── git_windows.go │ │ ├── git_windows_test.go │ │ ├── gitversion.go │ │ ├── gitversion_test.go │ │ ├── golang.go │ │ ├── golang_test.go │ │ ├── haskell.go │ │ ├── haskell_test.go │ │ ├── helm.go │ │ ├── helm_test.go │ │ ├── http.go │ │ ├── http_test.go │ │ ├── ipify.go │ │ ├── ipify_test.go │ │ ├── java.go │ │ ├── java_test.go │ │ ├── jujutsu.go │ │ ├── jujutsu_test.go │ │ ├── julia.go │ │ ├── julia_test.go │ │ ├── kotlin.go │ │ ├── kotlin_test.go │ │ ├── kubectl.go │ │ ├── kubectl_test.go │ │ ├── language.go │ │ ├── language_test.go │ │ ├── lastfm.go │ │ ├── lastfm_test.go │ │ ├── lua.go │ │ ├── lua_test.go │ │ ├── mercurial.go │ │ ├── mercurial_test.go │ │ ├── mojo.go │ │ ├── mojo_test.go │ │ ├── mvn.go │ │ ├── mvn_test.go │ │ ├── nba.go │ │ ├── nba_test.go │ │ ├── nbgv.go │ │ ├── nbgv_test.go │ │ ├── nightscout.go │ │ ├── nightscout_test.go │ │ ├── nim.go │ │ ├── nim_test.go │ │ ├── nixshell.go │ │ ├── nixshell_test.go │ │ ├── node.go │ │ ├── node_test.go │ │ ├── npm.go │ │ ├── npm_test.go │ │ ├── nx.go │ │ ├── ocaml.go │ │ ├── ocaml_test.go │ │ ├── options/ │ │ │ ├── map.go │ │ │ └── map_test.go │ │ ├── os.go │ │ ├── os_test.go │ │ ├── owm.go │ │ ├── owm_test.go │ │ ├── path.go │ │ ├── path_test.go │ │ ├── path_unix_test.go │ │ ├── path_windows_test.go │ │ ├── perl.go │ │ ├── perl_test.go │ │ ├── php.go │ │ ├── php_test.go │ │ ├── plastic.go │ │ ├── plastic_test.go │ │ ├── pnpm.go │ │ ├── pnpm_test.go │ │ ├── posh_git.go │ │ ├── posh_git_test.go │ │ ├── project.go │ │ ├── project_test.go │ │ ├── pulumi.go │ │ ├── pulumi_test.go │ │ ├── python.go │ │ ├── python_test.go │ │ ├── quasar.go │ │ ├── quasar_test.go │ │ ├── r.go │ │ ├── r_test.go │ │ ├── ramadan.go │ │ ├── ramadan_test.go │ │ ├── react.go │ │ ├── root.go │ │ ├── ruby.go │ │ ├── ruby_test.go │ │ ├── rust.go │ │ ├── rust_test.go │ │ ├── sapling.go │ │ ├── sapling_test.go │ │ ├── scm.go │ │ ├── scm_test.go │ │ ├── session.go │ │ ├── session_test.go │ │ ├── shell.go │ │ ├── shell_test.go │ │ ├── sitecore.go │ │ ├── sitecore_test.go │ │ ├── spotify.go │ │ ├── spotify_darwin.go │ │ ├── spotify_darwin_test.go │ │ ├── spotify_linux.go │ │ ├── spotify_linux_test.go │ │ ├── spotify_noop.go │ │ ├── spotify_test.go │ │ ├── spotify_windows.go │ │ ├── spotify_windows_test.go │ │ ├── status.go │ │ ├── status_test.go │ │ ├── strava.go │ │ ├── strava_test.go │ │ ├── svelte.go │ │ ├── svn.go │ │ ├── svn_test.go │ │ ├── swift.go │ │ ├── swift_test.go │ │ ├── sysinfo.go │ │ ├── sysinfo_test.go │ │ ├── talosctl.go │ │ ├── talosctl_test.go │ │ ├── taskwarrior.go │ │ ├── taskwarrior_test.go │ │ ├── tauri.go │ │ ├── terraform.go │ │ ├── terraform_test.go │ │ ├── text.go │ │ ├── text_test.go │ │ ├── time.go │ │ ├── time_test.go │ │ ├── todoist.go │ │ ├── todoist_test.go │ │ ├── ui5tooling.go │ │ ├── ui5tooling_test.go │ │ ├── umbraco.go │ │ ├── umbraco_test.go │ │ ├── unity.go │ │ ├── unity_test.go │ │ ├── upgrade.go │ │ ├── upgrade_test.go │ │ ├── v.go │ │ ├── v_test.go │ │ ├── vala.go │ │ ├── vala_test.go │ │ ├── wakatime.go │ │ ├── wakatime_test.go │ │ ├── winget.go │ │ ├── winget_test.go │ │ ├── winreg.go │ │ ├── winreg_test.go │ │ ├── withings.go │ │ ├── withings_test.go │ │ ├── xmake.go │ │ ├── xmake_test.go │ │ ├── yarn.go │ │ ├── yarn_test.go │ │ ├── ytm.go │ │ ├── ytm_test.go │ │ ├── zig.go │ │ └── zig_test.go │ ├── shell/ │ │ ├── bash.go │ │ ├── bash_test.go │ │ ├── cmd.go │ │ ├── cmd_test.go │ │ ├── code.go │ │ ├── constants.go │ │ ├── dsc.go │ │ ├── dsc_test.go │ │ ├── elvish.go │ │ ├── elvish_test.go │ │ ├── features.go │ │ ├── filesystem.go │ │ ├── fish.go │ │ ├── fish_test.go │ │ ├── formats.go │ │ ├── init.go │ │ ├── nu.go │ │ ├── nu_test.go │ │ ├── pwsh.go │ │ ├── pwsh_test.go │ │ ├── scripts/ │ │ │ ├── omp.bash │ │ │ ├── omp.elv │ │ │ ├── omp.fish │ │ │ ├── omp.lua │ │ │ ├── omp.nu │ │ │ ├── omp.ps1 │ │ │ ├── omp.xsh │ │ │ └── omp.zsh │ │ ├── xonsh.go │ │ ├── xonsh_test.go │ │ ├── zsh.go │ │ └── zsh_test.go │ ├── template/ │ │ ├── cache.go │ │ ├── compare.go │ │ ├── compare_test.go │ │ ├── files.go │ │ ├── files_test.go │ │ ├── func_map.go │ │ ├── init.go │ │ ├── link.go │ │ ├── link_test.go │ │ ├── list.go │ │ ├── numbers.go │ │ ├── numbers_test.go │ │ ├── pool_test.go │ │ ├── random.go │ │ ├── random_test.go │ │ ├── reason.go │ │ ├── regex.go │ │ ├── render.go │ │ ├── round.go │ │ ├── round_test.go │ │ ├── strings.go │ │ ├── strings_test.go │ │ ├── text.go │ │ └── text_test.go │ ├── terminal/ │ │ ├── iterm.go │ │ ├── writer.go │ │ ├── writer_hyperlink_test.go │ │ └── writer_test.go │ ├── test/ │ │ ├── AzureRmContext.json │ │ ├── azureProfile.json │ │ ├── empty.nuspec │ │ ├── go.work │ │ ├── invalid.nuspec │ │ ├── jandedobbeleer-palette.omp.json │ │ ├── jandedobbeleer.omp.json │ │ ├── kubectl.yml │ │ ├── nba/ │ │ │ ├── schedule.json │ │ │ └── score.json │ │ ├── oh-my-posh.psd1 │ │ ├── signing/ │ │ │ ├── checksums.txt │ │ │ ├── checksums.txt.invalid.sig │ │ │ └── checksums.txt.sig │ │ ├── terraform.tfstate │ │ ├── umbraco/ │ │ │ ├── ANonUmbracoProject.csproj │ │ │ ├── MyProject.csproj │ │ │ ├── web.config │ │ │ └── web.old.config │ │ ├── valid.nuspec │ │ └── versions.tf │ ├── text/ │ │ ├── builder.go │ │ ├── percentage.go │ │ └── percentage_test.go │ └── winres/ │ └── winres.json ├── themes/ │ ├── 1_shell.omp.json │ ├── M365Princess.omp.json │ ├── agnoster.minimal.omp.json │ ├── agnoster.omp.json │ ├── agnosterplus.omp.json │ ├── aliens.omp.json │ ├── amro.omp.json │ ├── atomic.omp.json │ ├── atomicBit.omp.json │ ├── avit.omp.json │ ├── blue-owl.omp.json │ ├── blueish.omp.json │ ├── bubbles.omp.json │ ├── bubblesextra.omp.json │ ├── bubblesline.omp.json │ ├── capr4n.omp.json │ ├── catppuccin.omp.json │ ├── catppuccin_frappe.omp.json │ ├── catppuccin_latte.omp.json │ ├── catppuccin_macchiato.omp.json │ ├── catppuccin_mocha.omp.json │ ├── cert.omp.json │ ├── chips.omp.json │ ├── cinnamon.omp.json │ ├── clean-detailed.omp.json │ ├── cloud-context.omp.json │ ├── cloud-native-azure.omp.json │ ├── cobalt2.omp.json │ ├── craver.omp.json │ ├── darkblood.omp.json │ ├── devious-diamonds.omp.yaml │ ├── di4am0nd.omp.json │ ├── dracula.omp.json │ ├── easy-term.omp.json │ ├── emodipt-extend.omp.json │ ├── emodipt.omp.json │ ├── fish.omp.json │ ├── free-ukraine.omp.json │ ├── froczh.omp.json │ ├── glowsticks.omp.yaml │ ├── gmay.omp.json │ ├── grandpa-style.omp.json │ ├── gruvbox.omp.json │ ├── half-life.omp.json │ ├── honukai.omp.json │ ├── hotstick.minimal.omp.json │ ├── hul10.omp.json │ ├── hunk.omp.json │ ├── huvix.omp.json │ ├── if_tea.omp.json │ ├── illusi0n.omp.json │ ├── iterm2.omp.json │ ├── jandedobbeleer.omp.json │ ├── jblab_2021.omp.json │ ├── jonnychipz.omp.json │ ├── json.omp.json │ ├── jtracey93.omp.json │ ├── jv_sitecorian.omp.json │ ├── kali.omp.json │ ├── kushal.omp.json │ ├── lambda.omp.json │ ├── lambdageneration.omp.json │ ├── larserikfinholt.omp.json │ ├── lightgreen.omp.json │ ├── marcduiker.omp.json │ ├── markbull.omp.json │ ├── material.omp.json │ ├── microverse-power.omp.json │ ├── mojada.omp.json │ ├── montys.omp.json │ ├── mt.omp.json │ ├── multiverse-neon.omp.json │ ├── negligible.omp.json │ ├── neko.omp.json │ ├── night-owl.omp.json │ ├── nordtron.omp.json │ ├── nu4a.omp.json │ ├── onehalf.minimal.omp.json │ ├── paradox.omp.json │ ├── pararussel.omp.json │ ├── patriksvensson.omp.json │ ├── peru.omp.json │ ├── pixelrobots.omp.json │ ├── plague.omp.json │ ├── poshmon.omp.json │ ├── powerlevel10k_classic.omp.json │ ├── powerlevel10k_lean.omp.json │ ├── powerlevel10k_modern.omp.json │ ├── powerlevel10k_rainbow.omp.json │ ├── powerline.omp.json │ ├── probua.minimal.omp.json │ ├── pure.omp.json │ ├── quick-term.omp.json │ ├── remk.omp.json │ ├── robbyrussell.omp.json │ ├── rudolfs-dark.omp.json │ ├── rudolfs-light.omp.json │ ├── schema.json │ ├── sim-web.omp.json │ ├── slim.omp.json │ ├── slimfat.omp.json │ ├── smoothie.omp.json │ ├── sonicboom_dark.omp.json │ ├── sonicboom_light.omp.json │ ├── sorin.omp.json │ ├── space.omp.json │ ├── spaceship.omp.json │ ├── star.omp.json │ ├── stelbent-compact.minimal.omp.json │ ├── stelbent.minimal.omp.json │ ├── takuya.omp.json │ ├── the-unnamed.omp.json │ ├── thecyberden.omp.json │ ├── tiwahu.omp.json │ ├── tokyo.omp.json │ ├── tokyonight_storm.omp.json │ ├── tonybaloney.omp.json │ ├── uew.omp.json │ ├── unicorn.omp.json │ ├── velvet.omp.json │ ├── wholespace.omp.json │ ├── wopian.omp.json │ ├── xtoys.omp.json │ ├── ys.omp.json │ └── zash.omp.json └── website/ ├── .gitignore ├── README.md ├── api/ │ ├── .funcignore │ ├── .gitignore │ ├── auth/ │ │ ├── function.json │ │ └── index.js │ ├── data/ │ │ ├── .gitignore │ │ └── README.md │ ├── host.json │ ├── mcp/ │ │ ├── .gitignore │ │ ├── .well-known/ │ │ │ └── mcp.json │ │ ├── README.md │ │ ├── function.json │ │ ├── index.js │ │ ├── server.json │ │ └── validate-server.js │ ├── package.json │ ├── proxies.json │ ├── refresh/ │ │ ├── function.json │ │ └── index.js │ ├── shared/ │ │ ├── strava.js │ │ ├── validator.js │ │ └── withings.js │ └── test/ │ └── validator.test.js ├── blog/ │ ├── 2022-03-20-whats-new-1.mdx │ ├── 2022-03-27-whats-new-2.md │ ├── 2022-03-28-idiots-everywhere.md │ ├── 2022-05-19-whats-new-3.md │ ├── 2024-07-22-bash-rprompt.md │ └── 2025-12-28-oh-my-posh-claude-code-integration.md ├── docs/ │ ├── advanced/ │ │ └── mcp-server.mdx │ ├── auth.mdx │ ├── configuration/ │ │ ├── block.mdx │ │ ├── colors.mdx │ │ ├── debug-prompt.mdx │ │ ├── general.mdx │ │ ├── introduction.mdx │ │ ├── line-error.mdx │ │ ├── sample.mdx │ │ ├── secondary-prompt.mdx │ │ ├── segment.mdx │ │ ├── templates.mdx │ │ ├── title.mdx │ │ ├── tooltips.mdx │ │ └── transient.mdx │ ├── contributing/ │ │ ├── git.mdx │ │ ├── plastic.mdx │ │ ├── segment.mdx │ │ └── started.mdx │ ├── contributors.md │ ├── dsc.md │ ├── experimental/ │ │ └── streaming.mdx │ ├── faq.mdx │ ├── installation/ │ │ ├── customize.mdx │ │ ├── fonts.mdx │ │ ├── homebrew.md │ │ ├── linux.mdx │ │ ├── macos.mdx │ │ ├── next.md │ │ ├── prompt.mdx │ │ ├── upgrade.mdx │ │ └── windows.mdx │ ├── migrating-module.md │ ├── segments/ │ │ ├── cli/ │ │ │ ├── angular.mdx │ │ │ ├── argocd.mdx │ │ │ ├── aurelia.mdx │ │ │ ├── bazel.mdx │ │ │ ├── buf.mdx │ │ │ ├── bun.mdx │ │ │ ├── claude.mdx │ │ │ ├── cmake.mdx │ │ │ ├── copilot.mdx │ │ │ ├── deno.mdx │ │ │ ├── docker.mdx │ │ │ ├── firebase.mdx │ │ │ ├── flutter.mdx │ │ │ ├── gitversion.mdx │ │ │ ├── helm.mdx │ │ │ ├── kubectl.mdx │ │ │ ├── mvn.mdx │ │ │ ├── nbgv.mdx │ │ │ ├── nix-shell.mdx │ │ │ ├── npm.mdx │ │ │ ├── nx.mdx │ │ │ ├── pnpm.mdx │ │ │ ├── quasar.mdx │ │ │ ├── react.mdx │ │ │ ├── svelte.mdx │ │ │ ├── talosctl.mdx │ │ │ ├── taskwarrior.mdx │ │ │ ├── tauri.mdx │ │ │ ├── terraform.mdx │ │ │ ├── ui5tooling.mdx │ │ │ ├── umbraco.mdx │ │ │ ├── unity.mdx │ │ │ ├── xmake.mdx │ │ │ └── yarn.mdx │ │ ├── cloud/ │ │ │ ├── aws.mdx │ │ │ ├── az.mdx │ │ │ ├── azd.mdx │ │ │ ├── azfunc.mdx │ │ │ ├── cds.mdx │ │ │ ├── cf.mdx │ │ │ ├── cftarget.mdx │ │ │ ├── gcp.mdx │ │ │ ├── pulumi.mdx │ │ │ └── sitecore.mdx │ │ ├── health/ │ │ │ ├── nightscout.mdx │ │ │ ├── ramadan.mdx │ │ │ ├── strava.mdx │ │ │ └── withings.mdx │ │ ├── languages/ │ │ │ ├── clojure.mdx │ │ │ ├── crystal.mdx │ │ │ ├── dart.mdx │ │ │ ├── dotnet.mdx │ │ │ ├── elixir.mdx │ │ │ ├── fortran.mdx │ │ │ ├── golang.mdx │ │ │ ├── haskell.mdx │ │ │ ├── java.mdx │ │ │ ├── julia.mdx │ │ │ ├── kotlin.mdx │ │ │ ├── lua.mdx │ │ │ ├── mojo.mdx │ │ │ ├── nim.mdx │ │ │ ├── node.mdx │ │ │ ├── ocaml.mdx │ │ │ ├── perl.mdx │ │ │ ├── php.mdx │ │ │ ├── python.mdx │ │ │ ├── r.mdx │ │ │ ├── ruby.mdx │ │ │ ├── rust.mdx │ │ │ ├── swift.mdx │ │ │ ├── v.mdx │ │ │ ├── vala.mdx │ │ │ └── zig.mdx │ │ ├── music/ │ │ │ ├── lastfm.mdx │ │ │ ├── spotify.mdx │ │ │ └── ytm.mdx │ │ ├── scm/ │ │ │ ├── fossil.mdx │ │ │ ├── git.mdx │ │ │ ├── jujutsu.mdx │ │ │ ├── mercurial.mdx │ │ │ ├── plastic.mdx │ │ │ ├── sapling.mdx │ │ │ └── svn.mdx │ │ ├── system/ │ │ │ ├── battery.mdx │ │ │ ├── connection.mdx │ │ │ ├── executiontime.mdx │ │ │ ├── os.mdx │ │ │ ├── path.mdx │ │ │ ├── project.mdx │ │ │ ├── root.mdx │ │ │ ├── session.mdx │ │ │ ├── shell.mdx │ │ │ ├── status.mdx │ │ │ ├── sysinfo.mdx │ │ │ ├── text.mdx │ │ │ ├── time.mdx │ │ │ ├── upgrade.mdx │ │ │ ├── winget.mdx │ │ │ └── winreg.mdx │ │ └── web/ │ │ ├── brewfather.mdx │ │ ├── carbonintensity.mdx │ │ ├── http.mdx │ │ ├── ipify.mdx │ │ ├── nba.mdx │ │ ├── owm.mdx │ │ ├── todoist.mdx │ │ └── wakatime.mdx │ ├── share-theme.md │ └── themes.md ├── docusaurus.config.js ├── export_themes.mjs ├── package.json ├── plugins/ │ └── appinsights/ │ ├── analytics.js │ └── index.js ├── sidebars.js ├── src/ │ ├── components/ │ │ ├── Auth.js │ │ └── Config.js │ ├── css/ │ │ ├── custom.css │ │ └── prism-rose-pine-moon.css │ └── pages/ │ ├── index.js │ ├── privacy.mdx │ └── styles.module.css ├── static/ │ ├── .nojekyll │ ├── codepoints.csv │ ├── img/ │ │ └── themes/ │ │ └── .keep │ ├── install.ps1 │ └── install.sh └── staticwebapp.config.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .all-contributorsrc ================================================ { "projectName": "oh-my-posh", "projectOwner": "JanDeDobbeleer", "repoType": "github", "repoHost": "https://github.com", "files": [ "website/docs/contributors.md" ], "imageSize": 100, "commit": true, "commitConvention": "angular", "contributors": [ { "login": "lnu", "name": "Laurent Nullens", "avatar_url": "https://avatars.githubusercontent.com/u/1829553?v=4", "profile": "https://github.com/lnu", "contributions": [ "code", "design", "doc" ] }, { "login": "TravisTX", "name": "Travis Collins", "avatar_url": "https://avatars.githubusercontent.com/u/934490?v=4", "profile": "https://github.com/TravisTX", "contributions": [ "code" ] }, { "login": "jos3s", "name": "José Ulisses", "avatar_url": "https://avatars.githubusercontent.com/u/50359547?v=4", "profile": "https://github.com/jos3s", "contributions": [ "code" ] }, { "login": "nwykes", "name": "Nathan Wykes", "avatar_url": "https://avatars.githubusercontent.com/u/593993?v=4", "profile": "https://github.com/nwykes", "contributions": [ "code" ] }, { "login": "tillig", "name": "Travis Illig", "avatar_url": "https://avatars.githubusercontent.com/u/1156571?v=4", "profile": "http://www.paraesthesia.com/", "contributions": [ "code" ] }, { "login": "evilz", "name": "Vincent B.", "avatar_url": "https://avatars.githubusercontent.com/u/2937862?v=4", "profile": "http://www.evilznet.com/", "contributions": [ "code" ] }, { "login": "erclu", "name": "Luca Ercole", "avatar_url": "https://avatars.githubusercontent.com/u/30255227?v=4", "profile": "https://erclu.github.io/cv/", "contributions": [ "code" ] }, { "login": "LarsBauer", "name": "Lars Bauer", "avatar_url": "https://avatars.githubusercontent.com/u/3920045?v=4", "profile": "https://larsbauer.xyz/", "contributions": [ "design" ] }, { "login": "RobCannon", "name": "Rob Cannon", "avatar_url": "https://avatars.githubusercontent.com/u/189862?v=4", "profile": "https://github.com/RobCannon", "contributions": [ "code" ] }, { "login": "Vixb1122", "name": "Vixb", "avatar_url": "https://avatars.githubusercontent.com/u/17810492?v=4", "profile": "https://github.com/Vixb1122", "contributions": [ "doc" ] }, { "login": "zeyugao", "name": "Elsa Granger", "avatar_url": "https://avatars.githubusercontent.com/u/6374697?v=4", "profile": "https://github.com/zeyugao", "contributions": [ "design" ] }, { "login": "softweaprograma", "name": "Anthony G", "avatar_url": "https://avatars.githubusercontent.com/u/35231092?v=4", "profile": "https://github.com/softweaprograma", "contributions": [ "design" ] }, { "login": "gitolicious", "name": "gitolicious", "avatar_url": "https://avatars.githubusercontent.com/u/26963495?v=4", "profile": "https://github.com/gitolicious", "contributions": [ "code" ] }, { "login": "irdkwmnsb", "name": "Maxim", "avatar_url": "https://avatars.githubusercontent.com/u/8657078?v=4", "profile": "https://alzhanov.ru/", "contributions": [ "design" ] }, { "login": "PIYUSH194", "name": "PIYUSH194", "avatar_url": "https://avatars.githubusercontent.com/u/2896456?v=4", "profile": "https://github.com/PIYUSH194", "contributions": [ "code" ] }, { "login": "97krihop", "name": "97krihop", "avatar_url": "https://avatars.githubusercontent.com/u/24739853?v=4", "profile": "https://github.com/97krihop", "contributions": [ "doc" ] }, { "login": "stefanes", "name": "Stefan", "avatar_url": "https://avatars.githubusercontent.com/u/5484354?v=4", "profile": "https://github.com/stefanes", "contributions": [ "design", "code" ] }, { "login": "moritz-meier", "name": "Moritz Meier", "avatar_url": "https://avatars.githubusercontent.com/u/60762067?v=4", "profile": "https://github.com/moritz-meier", "contributions": [ "code" ] }, { "login": "jetersen", "name": "Joseph Petersen", "avatar_url": "https://avatars.githubusercontent.com/u/1661688?v=4", "profile": "https://github.com/jetersen", "contributions": [ "code" ] }, { "login": "Goliaita", "name": "Davide Basile", "avatar_url": "https://avatars.githubusercontent.com/u/11245411?v=4", "profile": "https://github.com/Goliaita", "contributions": [ "code" ] }, { "login": "sukso96100", "name": "Youngbin Han", "avatar_url": "https://avatars.githubusercontent.com/u/1916739?v=4", "profile": "http://youngbin.xyz/", "contributions": [ "design" ] }, { "login": "mateusnssn", "name": "Mateus Nunes", "avatar_url": "https://avatars.githubusercontent.com/u/69170710?v=4", "profile": "https://mateusnssp.github.io/mateusnssp/", "contributions": [ "design" ] }, { "login": "PixelRobots", "name": "PixelRobots", "avatar_url": "https://avatars.githubusercontent.com/u/22979170?v=4", "profile": "https://pixelrobots.co.uk/", "contributions": [ "design" ] }, { "login": "RishabhSood", "name": "RishabhSood", "avatar_url": "https://avatars.githubusercontent.com/u/55499929?v=4", "profile": "https://github.com/RishabhSood", "contributions": [ "design" ] }, { "login": "SagarYadav17", "name": "Sagar Yadav", "avatar_url": "https://avatars.githubusercontent.com/u/47110215?v=4", "profile": "https://github.com/SagarYadav17", "contributions": [ "design" ] }, { "login": "WolfspiritM", "name": "Adrian", "avatar_url": "https://avatars.githubusercontent.com/u/5904171?v=4", "profile": "https://github.com/WolfspiritM", "contributions": [ "code" ] }, { "login": "MJECloud", "name": "Maurice", "avatar_url": "https://avatars.githubusercontent.com/u/22131101?v=4", "profile": "https://github.com/MJECloud", "contributions": [ "code" ] }, { "login": "samuelfahrngruber", "name": "samuelfahrngruber", "avatar_url": "https://avatars.githubusercontent.com/u/35682879?v=4", "profile": "https://github.com/samuelfahrngruber", "contributions": [ "code" ] }, { "login": "zilmarr", "name": "Zilmar de Souza Junior", "avatar_url": "https://avatars.githubusercontent.com/u/5557367?v=4", "profile": "https://github.com/zilmarr", "contributions": [ "design" ] }, { "login": "AsafMah", "name": "AsafMah", "avatar_url": "https://avatars.githubusercontent.com/u/6424271?v=4", "profile": "https://github.com/AsafMah", "contributions": [ "code" ] }, { "login": "cinnamon-msft", "name": "Kayla Cinnamon", "avatar_url": "https://avatars.githubusercontent.com/u/48369326?v=4", "profile": "https://github.com/cinnamon-msft", "contributions": [ "design", "code", "doc" ] }, { "login": "cbargren", "name": "Chris Bargren", "avatar_url": "https://avatars.githubusercontent.com/u/1050712?v=4", "profile": "https://github.com/cbargren", "contributions": [ "code" ] }, { "login": "tonybaloney", "name": "Anthony Shaw", "avatar_url": "https://avatars.githubusercontent.com/u/1532417?v=4", "profile": "https://tonybaloney.github.io/", "contributions": [ "design" ] }, { "login": "mifieldxu", "name": "Mifield", "avatar_url": "https://avatars.githubusercontent.com/u/5520179?v=4", "profile": "https://github.com/mifieldxu", "contributions": [ "doc" ] }, { "login": "benallred", "name": "Ben Allred", "avatar_url": "https://avatars.githubusercontent.com/u/3902274?v=4", "profile": "https://github.com/benallred", "contributions": [ "doc" ] }, { "login": "riazXrazor", "name": "Riaz Laskar", "avatar_url": "https://avatars.githubusercontent.com/u/13194363?v=4", "profile": "https://riazxrazor.herokuapp.com/", "contributions": [ "doc" ] }, { "login": "Don-Vito", "name": "Don-Vito", "avatar_url": "https://avatars.githubusercontent.com/u/4639110?v=4", "profile": "https://github.com/Don-Vito", "contributions": [ "doc" ] }, { "login": "FabianEscarate", "name": "Fabian Roberto Escarate", "avatar_url": "https://avatars.githubusercontent.com/u/19978896?v=4", "profile": "https://github.com/FabianEscarate", "contributions": [ "design" ] }, { "login": "xt0rted", "name": "Brian Surowiec", "avatar_url": "https://avatars.githubusercontent.com/u/831974?v=4", "profile": "https://github.com/xt0rted", "contributions": [ "code" ] }, { "login": "ojullien", "name": "Olivier Jullien", "avatar_url": "https://avatars.githubusercontent.com/u/3778194?v=4", "profile": "https://twitter.com/OJullien", "contributions": [ "code" ] }, { "login": "cdonnellytx", "name": "Chris Donnelly", "avatar_url": "https://avatars.githubusercontent.com/u/183046?v=4", "profile": "https://github.com/cdonnellytx", "contributions": [ "code" ] }, { "login": "KyleCrowley", "name": "Kyle Crowley", "avatar_url": "https://avatars.githubusercontent.com/u/6757487?v=4", "profile": "https://github.com/KyleCrowley", "contributions": [ "code" ] }, { "login": "gitolicious", "name": "gitolicious", "avatar_url": "https://avatars.githubusercontent.com/u/26963495?v=4", "profile": "https://github.com/gitolicious", "contributions": [ "code" ] }, { "login": "jeroen7s", "name": "Jeroen Evens", "avatar_url": "https://avatars.githubusercontent.com/u/10954827?v=4", "profile": "https://github.com/jeroen7s", "contributions": [ "doc" ] }, { "login": "equinox", "name": "equinox", "avatar_url": "https://avatars.githubusercontent.com/u/6139999?v=4", "profile": "https://github.com/equinox", "contributions": [ "doc" ] }, { "login": "DamianoPellegrini", "name": "Damiano Pellegrini", "avatar_url": "https://avatars.githubusercontent.com/u/41305552?v=4", "profile": "https://github.com/DamianoPellegrini", "contributions": [ "design" ] }, { "login": "timon-schelling", "name": "Timon Schelling", "avatar_url": "https://avatars.githubusercontent.com/u/36821505?v=4", "profile": "https://timokrates.de/", "contributions": [ "design" ] }, { "login": "zeyugao", "name": "Elsa Granger", "avatar_url": "https://avatars.githubusercontent.com/u/6374697?v=4", "profile": "https://github.com/zeyugao", "contributions": [ "design" ] }, { "login": "Daksh777", "name": "Daksh P. Jain", "avatar_url": "https://avatars.githubusercontent.com/u/43648146?v=4", "profile": "https://daksh.eu.org/", "contributions": [ "doc" ] }, { "login": "boarder2", "name": "Willie Zutz", "avatar_url": "https://avatars.githubusercontent.com/u/19351?v=4", "profile": "http://bit-shift.com/", "contributions": [ "doc" ] }, { "login": "uruz-7", "name": "uruz-7", "avatar_url": "https://avatars.githubusercontent.com/u/15071454?v=4", "profile": "https://github.com/uruz-7", "contributions": [ "design" ] }, { "login": "beppler", "name": "Carlos Alberto Costa Beppler", "avatar_url": "https://avatars.githubusercontent.com/u/66092?v=4", "profile": "https://github.com/beppler", "contributions": [ "code" ] }, { "login": "sky96111", "name": "sky96111", "avatar_url": "https://avatars.githubusercontent.com/u/22412214?v=4", "profile": "https://github.com/sky96111", "contributions": [ "design" ] }, { "login": "jantielens", "name": "Jan Tielens", "avatar_url": "https://avatars.githubusercontent.com/u/9884103?v=4", "profile": "http://j.tlns.be/", "contributions": [ "doc" ] }, { "login": "shedric1", "name": "shedric1", "avatar_url": "https://avatars.githubusercontent.com/u/56672838?v=4", "profile": "https://github.com/shedric1", "contributions": [ "code" ] }, { "login": "sectorogo", "name": "sectorogo", "avatar_url": "https://avatars.githubusercontent.com/u/32959212?v=4", "profile": "https://github.com/sectorogo", "contributions": [ "design" ] }, { "login": "phil-scott-78", "name": "Phil Scott", "avatar_url": "https://avatars.githubusercontent.com/u/2447331?v=4", "profile": "https://github.com/phil-scott-78", "contributions": [ "design" ] }, { "login": "suuus", "name": "Suus", "avatar_url": "https://avatars.githubusercontent.com/u/40822355?v=4", "profile": "https://suuu.us/", "contributions": [ "doc" ] }, { "login": "wopian", "name": "James Harris", "avatar_url": "https://avatars.githubusercontent.com/u/3440094?v=4", "profile": "https://wopian.me/", "contributions": [ "design" ] }, { "login": "mdlopresti", "name": "Michael LoPresti", "avatar_url": "https://avatars.githubusercontent.com/u/1293090?v=4", "profile": "https://github.com/mdlopresti", "contributions": [ "code" ] }, { "login": "floh96", "name": "Florian Heberl", "avatar_url": "https://avatars.githubusercontent.com/u/49693964?v=4", "profile": "https://github.com/floh96", "contributions": [ "doc" ] }, { "login": "relativityhd", "name": "Tobias Hölzer", "avatar_url": "https://avatars.githubusercontent.com/u/37540371?v=4", "profile": "http://tobiashoelzer.dynu.net", "contributions": [ "doc", "code" ] }, { "login": "h4iku", "name": "Reza Gharibi", "avatar_url": "https://avatars.githubusercontent.com/u/3812788?v=4", "profile": "https://h4iku.github.io", "contributions": [ "doc" ] }, { "login": "JustinGrote", "name": "Justin Grote", "avatar_url": "https://avatars.githubusercontent.com/u/15258962?v=4", "profile": "https://justingrote.github.io", "contributions": [ "doc" ] }, { "login": "henry-js", "name": "James", "avatar_url": "https://avatars.githubusercontent.com/u/79054685?v=4", "profile": "https://github.com/henry-js", "contributions": [ "doc" ] }, { "login": "iarejenius", "name": "Timothy Wittig", "avatar_url": "https://avatars.githubusercontent.com/u/1031515?v=4", "profile": "https://wittig.dev", "contributions": [ "code" ] }, { "login": "Descalon", "name": "Nico Glas", "avatar_url": "https://avatars.githubusercontent.com/u/1098500?v=4", "profile": "https://github.com/Descalon", "contributions": [ "code" ] }, { "login": "hanskokx", "name": "Hans Kokx", "avatar_url": "https://avatars.githubusercontent.com/u/1911919?v=4", "profile": "https://github.com/hanskokx", "contributions": [ "doc" ] }, { "login": "alchatti", "name": "Majed Al-Chatti", "avatar_url": "https://avatars.githubusercontent.com/u/9209306?v=4", "profile": "http://alchatti.com", "contributions": [ "design" ] }, { "login": "Jan0660", "name": "Jan0660", "avatar_url": "https://avatars.githubusercontent.com/u/58996212?v=4", "profile": "https://jan0660.dev", "contributions": [ "code" ] }, { "login": "LuiseFreese", "name": "Luise Freese", "avatar_url": "https://avatars.githubusercontent.com/u/49960482?v=4", "profile": "http://www.m365princess.com", "contributions": [ "design" ] }, { "login": "asherber", "name": "Aaron Sherber", "avatar_url": "https://avatars.githubusercontent.com/u/5248041?v=4", "profile": "https://github.com/asherber", "contributions": [ "code" ] }, { "login": "SeanKilleen", "name": "Sean Killeen", "avatar_url": "https://avatars.githubusercontent.com/u/2148318?v=4", "profile": "http://SeanKilleen.com", "contributions": [ "doc" ] }, { "login": "NickCraver", "name": "Nick Craver", "avatar_url": "https://avatars.githubusercontent.com/u/454813?v=4", "profile": "https://nickcraver.com", "contributions": [ "code" ] }, { "login": "justin-vogt", "name": "Justin Vogt", "avatar_url": "https://avatars.githubusercontent.com/u/84424169?v=4", "profile": "https://github.com/justin-vogt", "contributions": [ "design" ] }, { "login": "TheOnlyTails", "name": "TheOnlyTails", "avatar_url": "https://avatars.githubusercontent.com/u/65342367?v=4", "profile": "http://theonlytails.com", "contributions": [ "ideas" ] }, { "login": "bewing", "name": "bewing", "avatar_url": "https://avatars.githubusercontent.com/u/4759896?v=4", "profile": "https://github.com/bewing", "contributions": [ "code" ] }, { "login": "shawnwildermuth", "name": "Shawn Wildermuth", "avatar_url": "https://avatars.githubusercontent.com/u/568272?v=4", "profile": "http://wildermuth.com", "contributions": [ "doc", "code" ] }, { "login": "onpikono", "name": "Ondrej Pinka", "avatar_url": "https://avatars.githubusercontent.com/u/25362465?v=4", "profile": "https://github.com/onpikono", "contributions": [ "doc" ] }, { "login": "kasuken", "name": "Emanuele Bartolesi", "avatar_url": "https://avatars.githubusercontent.com/u/2757486?v=4", "profile": "https://www.emanuelebartolesi.com", "contributions": [ "design" ] }, { "login": "qiansen1386", "name": "Paris Qian", "avatar_url": "https://avatars.githubusercontent.com/u/1759658?v=4", "profile": "https://qiansen1386.github.io", "contributions": [ "design" ] }, { "login": "tjackadams", "name": "Thomas Adams", "avatar_url": "https://avatars.githubusercontent.com/u/2307314?v=4", "profile": "https://blog.itadams.co.uk", "contributions": [ "code" ] }, { "login": "gschizas", "name": "George Schizas", "avatar_url": "https://avatars.githubusercontent.com/u/598065?v=4", "profile": "http://www.terrasoft.gr/", "contributions": [ "code", "design" ] }, { "login": "denelon", "name": "denelon", "avatar_url": "https://avatars.githubusercontent.com/u/61799811?v=4", "profile": "https://github.com/denelon", "contributions": [ "code" ] }, { "login": "AbdelrahmanHafez", "name": "Hafez", "avatar_url": "https://avatars.githubusercontent.com/u/19984935?v=4", "profile": "https://github.com/AbdelrahmanHafez", "contributions": [ "doc" ] }, { "login": "TedCrocker", "name": "Ted Ballou", "avatar_url": "https://avatars.githubusercontent.com/u/382001?v=4", "profile": "https://github.com/TedCrocker", "contributions": [ "code", "doc" ] }, { "login": "mikesigs", "name": "Mike Sigsworth", "avatar_url": "https://avatars.githubusercontent.com/u/811177?v=4", "profile": "https://discardchanges.com", "contributions": [ "code", "doc" ] }, { "login": "memcpy-rand-rand-rand", "name": "Will", "avatar_url": "https://avatars.githubusercontent.com/u/90210865?v=4", "profile": "https://github.com/memcpy-rand-rand-rand", "contributions": [ "code", "doc" ] }, { "login": "shanselman", "name": "Scott Hanselman", "avatar_url": "https://avatars.githubusercontent.com/u/2892?v=4", "profile": "http://www.hanselman.com", "contributions": [ "code", "doc" ] }, { "login": "hgreving", "name": "Harmjan Greving", "avatar_url": "https://avatars.githubusercontent.com/u/23560667?v=4", "profile": "https://github.com/hgreving", "contributions": [ "doc" ] }, { "login": "Khaos66", "name": "Khaos", "avatar_url": "https://avatars.githubusercontent.com/u/4013009?v=4", "profile": "https://github.com/Khaos66", "contributions": [ "code", "doc" ] }, { "login": "mattwojo", "name": "Matt Wojciakowski", "avatar_url": "https://avatars.githubusercontent.com/u/7566797?v=4", "profile": "http://mattwojo.github.io/", "contributions": [ "doc" ] }, { "login": "TheTaylorLee", "name": "TheTaylorLee", "avatar_url": "https://avatars.githubusercontent.com/u/53202926?v=4", "profile": "https://www.powershellgallery.com/profiles/TaylorLee", "contributions": [ "design" ] }, { "login": "PapiPeppers", "name": "Papi Peppers", "avatar_url": "https://avatars.githubusercontent.com/u/57047860?v=4", "profile": "https://github.com/PapiPeppers", "contributions": [ "design" ] }, { "login": "erresen", "name": "erresen", "avatar_url": "https://avatars.githubusercontent.com/u/5566441?v=4", "profile": "https://erresen.github.io", "contributions": [ "doc" ] }, { "login": "icy-comet", "name": "Aniket Teredesai", "avatar_url": "https://avatars.githubusercontent.com/u/50461557?v=4", "profile": "https://aniketteredesai.com", "contributions": [ "doc" ] }, { "login": "sdebruyn", "name": "Sam Debruyn", "avatar_url": "https://avatars.githubusercontent.com/u/963413?v=4", "profile": "https://debruyn.dev", "contributions": [ "code" ] }, { "login": "larserikfinholt", "name": "Lars Erik Finholt", "avatar_url": "https://avatars.githubusercontent.com/u/1328417?v=4", "profile": "https://github.com/larserikfinholt", "contributions": [ "code" ] }, { "login": "simorgh1", "name": "Bahram Maravandi", "avatar_url": "https://avatars.githubusercontent.com/u/5792905?v=4", "profile": "https://github.com/simorgh1", "contributions": [ "code" ] }, { "login": "calebjenkins", "name": "Caleb Jenkins", "avatar_url": "https://avatars.githubusercontent.com/u/211001?v=4", "profile": "http://developingux.com", "contributions": [ "ideas" ] }, { "login": "FlavienMacquignon", "name": "FlavienMacquignon", "avatar_url": "https://avatars.githubusercontent.com/u/70152975?v=4", "profile": "https://github.com/FlavienMacquignon", "contributions": [ "doc" ] }, { "login": "Victoria-DR", "name": "Victoria", "avatar_url": "https://avatars.githubusercontent.com/u/68347113?v=4", "profile": "https://github.com/Victoria-DR", "contributions": [ "design" ] }, { "login": "UlanaXY", "name": "Mikolaj", "avatar_url": "https://avatars.githubusercontent.com/u/12629308?v=4", "profile": "https://github.com/UlanaXY", "contributions": [ "doc" ] }, { "login": "markbullplus", "name": "markbull", "avatar_url": "https://avatars.githubusercontent.com/u/88931495?v=4", "profile": "https://github.com/markbullplus", "contributions": [ "design" ] }, { "login": "brian6932", "name": "Brian", "avatar_url": "https://avatars.githubusercontent.com/u/18603393?v=4", "profile": "https://github.com/brian6932", "contributions": [ "code" ] }, { "login": "patHyatt", "name": "Patrick Hyatt", "avatar_url": "https://avatars.githubusercontent.com/u/296125?v=4", "profile": "http://www.patrickhyatt.com", "contributions": [ "doc" ] }, { "login": "hezhizhen", "name": "Zhizhen He", "avatar_url": "https://avatars.githubusercontent.com/u/7611700?v=4", "profile": "https://github.com/hezhizhen", "contributions": [ "code" ] }, { "login": "jedwillick", "name": "Jed Willick", "avatar_url": "https://avatars.githubusercontent.com/u/85419773?v=4", "profile": "https://github.com/jedwillick", "contributions": [ "code" ] }, { "login": "eltociear", "name": "Ikko Ashimine", "avatar_url": "https://avatars.githubusercontent.com/u/22633385?v=4", "profile": "https://bandism.net/", "contributions": [ "doc" ] }, { "login": "CapularisPerpetua", "name": "Courtney Caldwell", "avatar_url": "https://avatars.githubusercontent.com/u/32304933?v=4", "profile": "https://prokopto.dev/", "contributions": [ "doc" ] }, { "login": "rfverbruggen", "name": "Robbert Verbruggen", "avatar_url": "https://avatars.githubusercontent.com/u/2320197?v=4", "profile": "https://github.com/rfverbruggen", "contributions": [ "code" ] }, { "login": "Merlin2001", "name": "Marcus Mangelsdorf", "avatar_url": "https://avatars.githubusercontent.com/u/13134791?v=4", "profile": "https://github.com/Merlin2001", "contributions": [ "doc" ] }, { "login": "andresrinivasan", "name": "André Srinivasan", "avatar_url": "https://avatars.githubusercontent.com/u/134301?v=4", "profile": "http://linkedin.com/andresrinivasan", "contributions": [ "doc" ] }, { "login": "ehawman-rosenberg", "name": "ehawman-rosenberg", "avatar_url": "https://avatars.githubusercontent.com/u/81652082?v=4", "profile": "https://github.com/ehawman-rosenberg", "contributions": [ "doc", "code" ] }, { "login": "claudiospizzi", "name": "Claudio Spizzi", "avatar_url": "https://avatars.githubusercontent.com/u/1934246?v=4", "profile": "https://spizzi.net/", "contributions": [ "doc" ] }, { "login": "estruyf", "name": "Elio Struyf", "avatar_url": "https://avatars.githubusercontent.com/u/2900833?v=4", "profile": "https://www.eliostruyf.com", "contributions": [ "doc", "code" ] }, { "login": "oalders", "name": "Olaf Alders", "avatar_url": "https://avatars.githubusercontent.com/u/96205?v=4", "profile": "https://www.olafalders.com/", "contributions": [ "doc" ] }, { "login": "DavidDeSloovere", "name": "David De Sloovere", "avatar_url": "https://avatars.githubusercontent.com/u/352626?v=4", "profile": "https://blog.deltacode.be", "contributions": [ "code" ] }, { "login": "LensPlaysGames", "name": "LensPlaysGames", "avatar_url": "https://avatars.githubusercontent.com/u/69637718?v=4", "profile": "https://lensor-radii.netlify.app", "contributions": [ "doc" ] }, { "login": "atakiya", "name": "Alex 'Avunia' Takiya", "avatar_url": "https://avatars.githubusercontent.com/u/6952402?v=4", "profile": "https://takiya.eu", "contributions": [ "code" ] }, { "login": "kenmorse", "name": "kenmorse", "avatar_url": "https://avatars.githubusercontent.com/u/63734484?v=4", "profile": "https://github.com/kenmorse", "contributions": [ "doc" ] }, { "login": "xadozuk", "name": "xadozuk", "avatar_url": "https://avatars.githubusercontent.com/u/780423?v=4", "profile": "https://github.com/xadozuk", "contributions": [ "code" ] }, { "login": "vedantmgoyal9", "name": "Vedant", "avatar_url": "https://avatars.githubusercontent.com/u/83997633?v=4", "profile": "https://bittu.eu.org", "contributions": [ "design", "code" ] }, { "login": "lewis-yeung", "name": "L. Yeung", "avatar_url": "https://avatars.githubusercontent.com/u/83903009?v=4", "profile": "https://github.com/lewis-yeung", "contributions": [ "code", "doc", "design" ] }, { "login": "antoson", "name": "Ondrej Antos", "avatar_url": "https://avatars.githubusercontent.com/u/36371990?v=4", "profile": "https://github.com/antoson", "contributions": [ "doc" ] }, { "login": "Bahnschrift", "name": "Bahnschrift", "avatar_url": "https://avatars.githubusercontent.com/u/31170809?v=4", "profile": "https://github.com/Bahnschrift", "contributions": [ "doc" ] }, { "login": "jakeboone02", "name": "Jake Boone", "avatar_url": "https://avatars.githubusercontent.com/u/366438?v=4", "profile": "https://github.com/jakeboone02", "contributions": [ "doc" ] }, { "login": "kapsiR", "name": "kapsiR", "avatar_url": "https://avatars.githubusercontent.com/u/7165033?v=4", "profile": "https://github.com/kapsiR", "contributions": [ "doc", "code" ] }, { "login": "csrakowski", "name": "Christiaan Rakowski", "avatar_url": "https://avatars.githubusercontent.com/u/1303967?v=4", "profile": "https://github.com/csrakowski", "contributions": [ "code", "doc" ] }, { "login": "mosullivan93", "name": "Mitchell J. O'Sullivan", "avatar_url": "https://avatars.githubusercontent.com/u/7676935?v=4", "profile": "https://github.com/mosullivan93", "contributions": [ "code", "doc" ] }, { "login": "felpel", "name": "Félix Pelletier", "avatar_url": "https://avatars.githubusercontent.com/u/5000004?v=4", "profile": "https://github.com/felpel", "contributions": [ "doc" ] }, { "login": "ralish", "name": "Samuel D. Leslie", "avatar_url": "https://avatars.githubusercontent.com/u/3214803?v=4", "profile": "https://nexiom.net/", "contributions": [ "code" ] }, { "login": "AjayKMehta", "name": "Ajay Mehta", "avatar_url": "https://avatars.githubusercontent.com/u/11180071?v=4", "profile": "https://github.com/AjayKMehta", "contributions": [ "code" ] }, { "login": "the-eduardo", "name": "the-eduardo", "avatar_url": "https://avatars.githubusercontent.com/u/40523695?v=4", "profile": "https://github.com/the-eduardo", "contributions": [ "doc" ] }, { "login": "antonpiatek", "name": "Anton Piatek", "avatar_url": "https://avatars.githubusercontent.com/u/175077?v=4", "profile": "https://github.com/antonpiatek", "contributions": [ "doc" ] }, { "login": "prodehghan", "name": "Mohammad Dehghan", "avatar_url": "https://avatars.githubusercontent.com/u/1384790?v=4", "profile": "https://careers.stackoverflow.com/dehghan", "contributions": [ "doc" ] }, { "login": "bhagerty", "name": "bhagerty", "avatar_url": "https://avatars.githubusercontent.com/u/7828454?v=4", "profile": "https://github.com/bhagerty", "contributions": [ "doc" ] }, { "login": "CodyScavenger", "name": "Cody Scavenger", "avatar_url": "https://avatars.githubusercontent.com/u/94334877?v=4", "profile": "https://github.com/CodyScavenger", "contributions": [ "doc" ] }, { "login": "FWest98", "name": "Floris Westerman", "avatar_url": "https://avatars.githubusercontent.com/u/1918658?v=4", "profile": "http://fwest98.nl/", "contributions": [ "code" ] }, { "login": "mjcarman", "name": "Michael Carman", "avatar_url": "https://avatars.githubusercontent.com/u/121028?v=4", "profile": "https://github.com/mjcarman", "contributions": [ "code", "doc" ] }, { "login": "entr0pia", "name": "风沐白", "avatar_url": "https://avatars.githubusercontent.com/u/30486766?v=4", "profile": "https://github.com/entr0pia", "contributions": [ "design" ] }, { "login": "schallm", "name": "Michael T. Schall", "avatar_url": "https://avatars.githubusercontent.com/u/331167?v=4", "profile": "https://github.com/schallm", "contributions": [ "design" ] }, { "login": "craiglpeters", "name": "Craig Peters", "avatar_url": "https://avatars.githubusercontent.com/u/9445180?v=4", "profile": "https://github.com/craiglpeters", "contributions": [ "doc" ] }, { "login": "dorian-li", "name": "Dongyu Li", "avatar_url": "https://avatars.githubusercontent.com/u/49279922?v=4", "profile": "https://github.com/dorian-li", "contributions": [ "design" ] }, { "login": "cyberbliss", "name": "Stephen Judd", "avatar_url": "https://avatars.githubusercontent.com/u/5401528?v=4", "profile": "https://github.com/cyberbliss", "contributions": [ "code" ] }, { "login": "douugdev", "name": "Douglas Silva", "avatar_url": "https://avatars.githubusercontent.com/u/59324692?v=4", "profile": "https://douug.dev", "contributions": [ "doc" ] }, { "login": "BoseSj", "name": "SJ Basak", "avatar_url": "https://avatars.githubusercontent.com/u/58129377?v=4", "profile": "https://github.com/BoseSj", "contributions": [ "design" ] }, { "login": "treed", "name": "Ted Reed", "avatar_url": "https://avatars.githubusercontent.com/u/71910?v=4", "profile": "http://tedreed.info", "contributions": [ "code", "doc" ] }, { "login": "asportnoy", "name": "Albert Portnoy", "avatar_url": "https://avatars.githubusercontent.com/u/14863373?v=4", "profile": "http://albertp.dev", "contributions": [ "code", "doc" ] }, { "login": "Lemorz56", "name": "Sebastian", "avatar_url": "https://avatars.githubusercontent.com/u/1346676?v=4", "profile": "https://www.msbrg.net/", "contributions": [ "code", "design", "doc" ] }, { "login": "mirsella", "name": "Lucas", "avatar_url": "https://avatars.githubusercontent.com/u/45905567?v=4", "profile": "https://github.com/mirsella", "contributions": [ "code", "design", "doc" ] }, { "login": "ethansocal", "name": "Ethan", "avatar_url": "https://avatars.githubusercontent.com/u/79533577?v=4", "profile": "https://github.com/ethansocal", "contributions": [ "doc" ] }, { "login": "astronaako", "name": "Mohamed Naamy", "avatar_url": "https://avatars.githubusercontent.com/u/18577543?v=4", "profile": "https://github.com/astronaako", "contributions": [ "design" ] }, { "login": "bend-n", "name": "bendn", "avatar_url": "https://avatars.githubusercontent.com/u/70787919?v=4", "profile": "http://bend-n.github.io", "contributions": [ "design" ] }, { "login": "davidanthoff", "name": "David Anthoff", "avatar_url": "https://avatars.githubusercontent.com/u/1036561?v=4", "profile": "http://www.david-anthoff.com", "contributions": [ "code", "doc" ] }, { "login": "jooooel", "name": "jooooel", "avatar_url": "https://avatars.githubusercontent.com/u/9303280?v=4", "profile": "https://github.com/jooooel", "contributions": [ "doc" ] }, { "login": "maxlandon", "name": "maxlandon", "avatar_url": "https://avatars.githubusercontent.com/u/25826036?v=4", "profile": "https://github.com/maxlandon", "contributions": [ "code" ] }, { "login": "lino-levan", "name": "Lino Le Van", "avatar_url": "https://avatars.githubusercontent.com/u/11367844?v=4", "profile": "https://linolevan.com", "contributions": [ "doc" ] }, { "login": "dvlprJobayer", "name": "Jobayer Ahammed Patwary", "avatar_url": "https://avatars.githubusercontent.com/u/76583359?v=4", "profile": "https://github.com/dvlprJobayer", "contributions": [ "design" ] }, { "login": "NoF0rte", "name": "NoF0rte", "avatar_url": "https://avatars.githubusercontent.com/u/64100993?v=4", "profile": "https://github.com/NoF0rte", "contributions": [ "code", "design", "doc" ] }, { "login": "LNKLEO", "name": "LNKLEO", "avatar_url": "https://avatars.githubusercontent.com/u/10334184?v=4", "profile": "https://github.com/LNKLEO", "contributions": [ "code", "design", "doc" ] }, { "login": "kamfaima", "name": "kamfaima", "avatar_url": "https://avatars.githubusercontent.com/u/23546392?v=4", "profile": "https://github.com/kamfaima", "contributions": [ "design" ] }, { "login": "dhrdlicka", "name": "David Hrdlička", "avatar_url": "https://avatars.githubusercontent.com/u/13226155?v=4", "profile": "https://github.com/dhrdlicka", "contributions": [ "code", "doc" ] }, { "login": "davidcourtney", "name": "David Courtney", "avatar_url": "https://avatars.githubusercontent.com/u/1019134?v=4", "profile": "http://davidcourtney.com", "contributions": [ "code", "design", "doc" ] }, { "login": "Jensdevloo", "name": "jensdevloo", "avatar_url": "https://avatars.githubusercontent.com/u/2276152?v=4", "profile": "https://github.com/Jensdevloo", "contributions": [ "doc" ] }, { "login": "thomasdoerr", "name": "Thomas Dörr", "avatar_url": "https://avatars.githubusercontent.com/u/6919685?v=4", "profile": "https://github.com/thomasdoerr", "contributions": [ "design" ] }, { "login": "SvenAelterman", "name": "Sven Aelterman", "avatar_url": "https://avatars.githubusercontent.com/u/17446043?v=4", "profile": "https://blog.aelterman.com", "contributions": [ "doc" ] }, { "login": "CodexLink", "name": "Janrey Licas", "avatar_url": "https://avatars.githubusercontent.com/u/5953927?v=4", "profile": "https://github.com/CodexLink", "contributions": [ "design", "doc", "code" ] }, { "login": "padilo", "name": "Pablo Díaz-López", "avatar_url": "https://avatars.githubusercontent.com/u/783959?v=4", "profile": "https://github.com/padilo", "contributions": [ "doc" ] }, { "login": "DarkMagicSource", "name": "Caitlyn Williams", "avatar_url": "https://avatars.githubusercontent.com/u/35950530?v=4", "profile": "https://github.com/DarkMagicSource", "contributions": [ "doc" ] }, { "login": "gork3n", "name": "Christopher Henderson", "avatar_url": "https://avatars.githubusercontent.com/u/1086155?v=4", "profile": "https://github.com/gork3n", "contributions": [ "design" ] }, { "login": "cabauman", "name": "Colt", "avatar_url": "https://avatars.githubusercontent.com/u/6819362?v=4", "profile": "https://www.coltbauman.com", "contributions": [ "code", "design", "doc" ] }, { "login": "craftzneko", "name": "craftzneko", "avatar_url": "https://avatars.githubusercontent.com/u/662108?v=4", "profile": "https://github.com/craftzneko", "contributions": [ "doc" ] }, { "login": "atlanswer", "name": "甘亭", "avatar_url": "https://avatars.githubusercontent.com/u/17683244?v=4", "profile": "http://waferlab.dev", "contributions": [ "doc" ] }, { "login": "Mertsch", "name": "Mertsch", "avatar_url": "https://avatars.githubusercontent.com/u/9402861?v=4", "profile": "https://github.com/Mertsch", "contributions": [ "doc" ] }, { "login": "marc2332", "name": "Marc Espín", "avatar_url": "https://avatars.githubusercontent.com/u/38158676?v=4", "profile": "https://mespin.me/", "contributions": [ "code" ] }, { "login": "ksdpmx", "name": "jasonz", "avatar_url": "https://avatars.githubusercontent.com/u/3256083?v=4", "profile": "https://github.com/ksdpmx", "contributions": [ "code", "doc" ] }, { "login": "bsiegert", "name": "Benny Siegert", "avatar_url": "https://avatars.githubusercontent.com/u/866330?v=4", "profile": "https://bentsukun.ch", "contributions": [ "code" ] }, { "login": "kema-dev", "name": "kema", "avatar_url": "https://avatars.githubusercontent.com/u/54537427?v=4", "profile": "http://www.kemadev.fr/fr/", "contributions": [ "code", "design", "doc" ] }, { "login": "mavaddat", "name": "Mavaddat Javid", "avatar_url": "https://avatars.githubusercontent.com/u/5055400?v=4", "profile": "http://mavaddat.ca", "contributions": [ "code" ] }, { "login": "iavael", "name": "Iavael", "avatar_url": "https://avatars.githubusercontent.com/u/905853?v=4", "profile": "https://iavael.name/", "contributions": [ "code" ] }, { "login": "Kushal-Chandar", "name": "Kushal-Chandar", "avatar_url": "https://avatars.githubusercontent.com/u/83660514?v=4", "profile": "https://github.com/Kushal-Chandar", "contributions": [ "design" ] }, { "login": "BigBear0812", "name": "Matthew Miller", "avatar_url": "https://avatars.githubusercontent.com/u/2429638?v=4", "profile": "http://www.project-miller.com/", "contributions": [ "code", "doc" ] }, { "login": "javidcf", "name": "Javier Dehesa", "avatar_url": "https://avatars.githubusercontent.com/u/1098280?v=4", "profile": "https://github.com/javidcf", "contributions": [ "code" ] }, { "login": "alexvy86", "name": "Alex Villarreal", "avatar_url": "https://avatars.githubusercontent.com/u/716334?v=4", "profile": "https://alex-v.blog/", "contributions": [ "code" ] }, { "login": "krzysdz", "name": "krzysdz", "avatar_url": "https://avatars.githubusercontent.com/u/12915102?v=4", "profile": "https://github.com/krzysdz", "contributions": [ "code", "design", "doc" ] }, { "login": "BasLijten", "name": "Bas Lijten", "avatar_url": "https://avatars.githubusercontent.com/u/11842067?v=4", "profile": "http://blog.baslijten.com", "contributions": [ "code", "design", "doc" ] }, { "login": "ParkerM", "name": "Parker Mauney", "avatar_url": "https://avatars.githubusercontent.com/u/5124113?v=4", "profile": "https://github.com/ParkerM", "contributions": [ "design" ] }, { "login": "gbrusella", "name": "Gonzalo Brusella", "avatar_url": "https://avatars.githubusercontent.com/u/115679?v=4", "profile": "http://www.brusella.com.ar", "contributions": [ "doc" ] }, { "login": "krokofant", "name": "Emil Sundin", "avatar_url": "https://avatars.githubusercontent.com/u/5908498?v=4", "profile": "https://github.com/krokofant", "contributions": [ "doc" ] }, { "login": "dysuby", "name": "dysuby", "avatar_url": "https://avatars.githubusercontent.com/u/26317510?v=4", "profile": "http://dysuby.github.io", "contributions": [ "doc" ] }, { "login": "dorokhin-bohdan", "name": "Bohdan Dorokhin", "avatar_url": "https://avatars.githubusercontent.com/u/24988081?v=4", "profile": "https://github.com/dorokhin-bohdan", "contributions": [ "code", "design", "doc" ] }, { "login": "CY-Pan", "name": "Ad Red", "avatar_url": "https://avatars.githubusercontent.com/u/59761962?v=4", "profile": "https://github.com/CY-Pan", "contributions": [ "design" ] }, { "login": "nopeless", "name": "nopeless", "avatar_url": "https://avatars.githubusercontent.com/u/38830903?v=4", "profile": "https://github.com/nopeless", "contributions": [ "code", "doc", "design" ] }, { "login": "vinhloc30796", "name": "Loc Nguyen", "avatar_url": "https://avatars.githubusercontent.com/u/19675202?v=4", "profile": "https://linkedin.com/in/vinhloc30796", "contributions": [ "doc" ] }, { "login": "Coder-Tavi", "name": "Tavi", "avatar_url": "https://avatars.githubusercontent.com/u/66774833?v=4", "profile": "https://tavis.page", "contributions": [ "doc" ] }, { "login": "NicholasDawson", "name": "Nick Dawson", "avatar_url": "https://avatars.githubusercontent.com/u/37987430?v=4", "profile": "http://ndawson.me", "contributions": [ "doc" ] }, { "login": "jntrnr", "name": "JT", "avatar_url": "https://avatars.githubusercontent.com/u/547158?v=4", "profile": "https://www.jntrnr.com/", "contributions": [ "code" ] }, { "login": "ChandanChainani", "name": "ChandanChainani", "avatar_url": "https://avatars.githubusercontent.com/u/28807775?v=4", "profile": "https://github.com/ChandanChainani", "contributions": [ "doc" ] }, { "login": "jenspinney", "name": "Jen Spinney", "avatar_url": "https://avatars.githubusercontent.com/u/3200507?v=4", "profile": "https://github.com/jenspinney", "contributions": [ "code", "doc" ] }, { "login": "rotu", "name": "Dan Rose", "avatar_url": "https://avatars.githubusercontent.com/u/119948?v=4", "profile": "https://github.com/rotu", "contributions": [ "code" ] }, { "login": "darthwalsh", "name": "Carl Walsh", "avatar_url": "https://avatars.githubusercontent.com/u/2829438?v=4", "profile": "https://carlwa.com", "contributions": [ "doc" ] }, { "login": "ercpereda", "name": "Ernesto R. C. Pereda", "avatar_url": "https://avatars.githubusercontent.com/u/13546685?v=4", "profile": "https://github.com/ercpereda", "contributions": [ "code", "design", "doc" ] }, { "login": "0Ky", "name": "cryptix", "avatar_url": "https://avatars.githubusercontent.com/u/16103757?v=4", "profile": "https://github.com/0Ky", "contributions": [ "doc" ] }, { "login": "ehawman", "name": "Evan Hawman", "avatar_url": "https://avatars.githubusercontent.com/u/52979227?v=4", "profile": "https://github.com/ehawman", "contributions": [ "design" ] }, { "login": "ZerdoX-x", "name": "Mark Lansky", "avatar_url": "https://avatars.githubusercontent.com/u/49815452?v=4", "profile": "https://zerdox.dev", "contributions": [ "design" ] }, { "login": "pulsation", "name": "pulsation", "avatar_url": "https://avatars.githubusercontent.com/u/1838397?v=4", "profile": "https://github.com/pulsation", "contributions": [ "code" ] }, { "login": "oriionn", "name": "orionsource", "avatar_url": "https://avatars.githubusercontent.com/u/38093786?v=4", "profile": "https://oriondev.fr", "contributions": [ "design" ] }, { "login": "CesarGBkR", "name": "Cesar Garduño", "avatar_url": "https://avatars.githubusercontent.com/u/99093357?v=4", "profile": "https://github.com/CesarGBkR", "contributions": [ "doc" ] }, { "login": "Adi-vig", "name": "Aditya Sakhare", "avatar_url": "https://avatars.githubusercontent.com/u/123308369?v=4", "profile": "https://github.com/Adi-vig", "contributions": [ "design" ] }, { "login": "deepak-dev-96", "name": "Deepak Dev", "avatar_url": "https://avatars.githubusercontent.com/u/134447761?v=4", "profile": "https://github.com/deepak-dev-96", "contributions": [ "doc" ] }, { "login": "warrenbuckley", "name": "Warren Buckley", "avatar_url": "https://avatars.githubusercontent.com/u/1389894?v=4", "profile": "http://creativewebspecialist.co.uk", "contributions": [ "code", "design", "doc" ] }, { "login": "LunarMarathon", "name": "LunarMarathon", "avatar_url": "https://avatars.githubusercontent.com/u/113847439?v=4", "profile": "https://github.com/LunarMarathon", "contributions": [ "doc" ] }, { "login": "ginglis13", "name": "Gavin Inglis", "avatar_url": "https://avatars.githubusercontent.com/u/43075615?v=4", "profile": "https://ginglis.me", "contributions": [ "code" ] }, { "login": "jaliyaudagedara", "name": "Jaliya Udagedara", "avatar_url": "https://avatars.githubusercontent.com/u/5653381?v=4", "profile": "http://jaliyaudagedara.blogspot.com", "contributions": [ "doc" ] }, { "login": "BPplays", "name": "BPplays", "avatar_url": "https://avatars.githubusercontent.com/u/58504799?v=4", "profile": "https://github.com/BPplays", "contributions": [ "code" ] }, { "login": "mateusz-bajorek", "name": "Mateusz Bajorek", "avatar_url": "https://avatars.githubusercontent.com/u/11185738?v=4", "profile": "https://github.com/mateusz-bajorek", "contributions": [ "code", "design", "doc" ] }, { "login": "joshbduncan", "name": "Josh Duncan", "avatar_url": "https://avatars.githubusercontent.com/u/44387852?v=4", "profile": "http://joshbduncan.com", "contributions": [ "doc" ] }, { "login": "princesaini", "name": "Prince Saini", "avatar_url": "https://avatars.githubusercontent.com/u/25565506?v=4", "profile": "https://github.com/princesaini", "contributions": [ "design" ] }, { "login": "fabriciojlm", "name": "fabriciojlm", "avatar_url": "https://avatars.githubusercontent.com/u/70244182?v=4", "profile": "https://www.linkedin.com/in/fabriciojuliano/", "contributions": [ "design" ] }, { "login": "SriRamanujam", "name": "Sri Ramanujam", "avatar_url": "https://avatars.githubusercontent.com/u/2983875?v=4", "profile": "https://github.com/SriRamanujam", "contributions": [ "code", "doc" ] }, { "login": "Juneezee", "name": "Eng Zer Jun", "avatar_url": "https://avatars.githubusercontent.com/u/20135478?v=4", "profile": "https://github.com/Juneezee", "contributions": [ "code" ] }, { "login": "AlexJPotter", "name": "Alex Potter", "avatar_url": "https://avatars.githubusercontent.com/u/14200888?v=4", "profile": "https://alexpotter.dev", "contributions": [ "code", "design", "doc" ] }, { "login": "mishmanners", "name": "Michelle Mannering", "avatar_url": "https://avatars.githubusercontent.com/u/36594527?v=4", "profile": "http://mishmanners.info", "contributions": [ "doc" ] }, { "login": "paulomorgado", "name": "Paulo Morgado", "avatar_url": "https://avatars.githubusercontent.com/u/470455?v=4", "profile": "https://github.com/paulomorgado", "contributions": [ "code", "doc" ] }, { "login": "joadoumie", "name": "joadoumie", "avatar_url": "https://avatars.githubusercontent.com/u/98557455?v=4", "profile": "https://github.com/joadoumie", "contributions": [ "code", "doc" ] }, { "login": "flanakin", "name": "Michael Flanakin", "avatar_url": "https://avatars.githubusercontent.com/u/399533?v=4", "profile": "http://about.me/flanakin", "contributions": [ "doc" ] }, { "login": "thiagoszbarros", "name": "Thiago Barros", "avatar_url": "https://avatars.githubusercontent.com/u/88802518?v=4", "profile": "https://www.linkedin.com/in/thiagobarros95/", "contributions": [ "design" ] }, { "login": "TendTo", "name": "Tend", "avatar_url": "https://avatars.githubusercontent.com/u/65033249?v=4", "profile": "https://github.com/TendTo", "contributions": [ "code", "design", "doc" ] }, { "login": "KibbeWater", "name": "Snow", "avatar_url": "https://avatars.githubusercontent.com/u/35224538?v=4", "profile": "https://kibbewater.com", "contributions": [ "code", "design", "doc" ] }, { "login": "randombenj", "name": "Benj Fassbind", "avatar_url": "https://avatars.githubusercontent.com/u/5184499?v=4", "profile": "https://github.com/randombenj", "contributions": [ "code" ] }, { "login": "liudonghua123", "name": "liudonghua", "avatar_url": "https://avatars.githubusercontent.com/u/2276718?v=4", "profile": "http://blog.liudonghua.top", "contributions": [ "code" ] }, { "login": "Somoy73", "name": "Somoy Subandhu", "avatar_url": "https://avatars.githubusercontent.com/u/40368688?v=4", "profile": "http://somoy.me", "contributions": [ "design" ] }, { "login": "oleksbabieiev", "name": "Oleksandr Babieiev", "avatar_url": "https://avatars.githubusercontent.com/u/64398691?v=4", "profile": "https://github.com/oleksbabieiev", "contributions": [ "code", "doc", "design" ] }, { "login": "mrbeardad", "name": "Heache Bear", "avatar_url": "https://avatars.githubusercontent.com/u/54128430?v=4", "profile": "https://github.com/mrbeardad", "contributions": [ "doc" ] }, { "login": "ChrisNSki", "name": "Christopher Narowski", "avatar_url": "https://avatars.githubusercontent.com/u/125232146?v=4", "profile": "http://ensif.com", "contributions": [ "design" ] }, { "login": "sino1641", "name": "Sin", "avatar_url": "https://avatars.githubusercontent.com/u/13870295?v=4", "profile": "https://github.com/sino1641", "contributions": [ "doc" ] }, { "login": "kkk669", "name": "Kenta Kubo", "avatar_url": "https://avatars.githubusercontent.com/u/601636?v=4", "profile": "https://kebo.xyz", "contributions": [ "doc" ] }, { "login": "mfedatto", "name": "MFedatto", "avatar_url": "https://avatars.githubusercontent.com/u/5623739?v=4", "profile": "http://mfedatto.com", "contributions": [ "doc" ] }, { "login": "RiikkaDream", "name": "Riikka", "avatar_url": "https://avatars.githubusercontent.com/u/56921531?v=4", "profile": "https://www.linkedin.com/in/riikka-l-861694b2/", "contributions": [ "doc" ] }, { "login": "srpmtt", "name": "srpmtt", "avatar_url": "https://avatars.githubusercontent.com/u/11175503?v=4", "profile": "https://github.com/srpmtt", "contributions": [ "code", "design", "doc" ] }, { "login": "Chris-Johnston", "name": "Chris Johnston", "avatar_url": "https://avatars.githubusercontent.com/u/16418643?v=4", "profile": "https://chris-johnston.me", "contributions": [ "doc" ] }, { "login": "Daimonion1980", "name": "Thomas", "avatar_url": "https://avatars.githubusercontent.com/u/12880413?v=4", "profile": "https://github.com/Daimonion1980", "contributions": [ "design" ] }, { "login": "VEERT00X", "name": "Veko", "avatar_url": "https://avatars.githubusercontent.com/u/72668825?v=4", "profile": "https://veert00x.com", "contributions": [ "doc" ] }, { "login": "lucascosti", "name": "Lucas Costi", "avatar_url": "https://avatars.githubusercontent.com/u/4434330?v=4", "profile": "https://lucascosti.com", "contributions": [ "code" ] }, { "login": "gergelyk", "name": "Grzegorz Krasoń", "avatar_url": "https://avatars.githubusercontent.com/u/11185582?v=4", "profile": "http://krason.dev/", "contributions": [ "code" ] }, { "login": "rockyoung", "name": "rockyoung", "avatar_url": "https://avatars.githubusercontent.com/u/1207971?v=4", "profile": "https://github.com/rockyoung", "contributions": [ "code", "doc" ] }, { "login": "shravanasati", "name": "Shravan Asati", "avatar_url": "https://avatars.githubusercontent.com/u/69118069?v=4", "profile": "https://github.com/shravanasati", "contributions": [ "design" ] }, { "login": "lzecca78", "name": "Luca Zecca", "avatar_url": "https://avatars.githubusercontent.com/u/3881844?v=4", "profile": "https://github.com/lzecca78", "contributions": [ "code", "design", "doc" ] }, { "login": "jreilly-lukava", "name": "Joshua Reilly", "avatar_url": "https://avatars.githubusercontent.com/u/30353736?v=4", "profile": "https://github.com/jreilly-lukava", "contributions": [ "code", "doc" ] }, { "login": "ivan-the-terrible", "name": "Ivan", "avatar_url": "https://avatars.githubusercontent.com/u/56458442?v=4", "profile": "https://ivan-the-terrible.github.io/", "contributions": [ "design", "doc", "code" ] }, { "login": "mountcount", "name": "mountcount", "avatar_url": "https://avatars.githubusercontent.com/u/166301065?v=4", "profile": "https://github.com/mountcount", "contributions": [ "doc" ] }, { "login": "Bondrake", "name": "Bondrake", "avatar_url": "https://avatars.githubusercontent.com/u/11696?v=4", "profile": "https://github.com/Bondrake", "contributions": [ "design", "code" ] }, { "login": "R00dRallec", "name": "R00dRallec", "avatar_url": "https://avatars.githubusercontent.com/u/9081954?v=4", "profile": "https://github.com/R00dRallec", "contributions": [ "code", "doc" ] }, { "login": "publicfacingusername", "name": "Justin Wolfington", "avatar_url": "https://avatars.githubusercontent.com/u/13956145?v=4", "profile": "https://github.com/publicfacingusername", "contributions": [ "code" ] }, { "login": "jtracey93", "name": "Jack Tracey", "avatar_url": "https://avatars.githubusercontent.com/u/41163455?v=4", "profile": "https://bio.link/jacktracey", "contributions": [ "design" ] }, { "login": "MarkDaveny", "name": "MarkDaveny", "avatar_url": "https://avatars.githubusercontent.com/u/168091250?v=4", "profile": "https://github.com/MarkDaveny", "contributions": [ "code", "design", "doc" ] }, { "login": "tiwahu", "name": "Timothy Huber", "avatar_url": "https://avatars.githubusercontent.com/u/590564?v=4", "profile": "http://www.tiwahu.com/", "contributions": [ "design" ] }, { "login": "YashJM", "name": "Yash Mistry", "avatar_url": "https://avatars.githubusercontent.com/u/63824041?v=4", "profile": "http://yashjmistry.me", "contributions": [ "design" ] }, { "login": "jlabonski", "name": "Jeffrey Labonski", "avatar_url": "https://avatars.githubusercontent.com/u/2981369?v=4", "profile": "https://github.com/jlabonski", "contributions": [ "code", "doc" ] }, { "login": "herbygillot", "name": "Herby Gillot", "avatar_url": "https://avatars.githubusercontent.com/u/618376?v=4", "profile": "https://github.com/herbygillot", "contributions": [ "doc" ] }, { "login": "arjan-s", "name": "arjan-s", "avatar_url": "https://avatars.githubusercontent.com/u/10400299?v=4", "profile": "https://github.com/arjan-s", "contributions": [ "code", "design", "doc" ] }, { "login": "0323pin", "name": "pin", "avatar_url": "https://avatars.githubusercontent.com/u/90570748?v=4", "profile": "https://github.com/0323pin", "contributions": [ "code" ] }, { "login": "FireIsGood", "name": "FireIsGood", "avatar_url": "https://avatars.githubusercontent.com/u/109556932?v=4", "profile": "http://fireis.dev", "contributions": [ "doc", "code" ] }, { "login": "Joxtacy", "name": "Jesper Hasselquist", "avatar_url": "https://avatars.githubusercontent.com/u/10127673?v=4", "profile": "https://github.com/Joxtacy", "contributions": [ "code", "design", "doc" ] }, { "login": "aaronpowell", "name": "Aaron Powell", "avatar_url": "https://avatars.githubusercontent.com/u/434140?v=4", "profile": "https://www.aaron-powell.com", "contributions": [ "code", "design", "doc" ] }, { "login": "Dartypier", "name": "Jacopo Zecchi", "avatar_url": "https://avatars.githubusercontent.com/u/22201626?v=4", "profile": "https://github.com/Dartypier", "contributions": [ "doc" ] }, { "login": "rose-m", "name": "Michael Rose", "avatar_url": "https://avatars.githubusercontent.com/u/4354632?v=4", "profile": "https://github.com/rose-m", "contributions": [ "code", "doc" ] }, { "login": "denehoffman", "name": "Nathaniel D. Hoffman", "avatar_url": "https://avatars.githubusercontent.com/u/36977879?v=4", "profile": "http://denehoffman.com", "contributions": [ "code", "doc" ] }, { "login": "michaelschwobe", "name": "Michael Schwobe", "avatar_url": "https://avatars.githubusercontent.com/u/926242?v=4", "profile": "https://schwobe.dev", "contributions": [ "code", "design", "doc" ] }, { "login": "Nibodhika", "name": "Nibodhika", "avatar_url": "https://avatars.githubusercontent.com/u/729967?v=4", "profile": "https://github.com/Nibodhika", "contributions": [ "code", "doc" ] }, { "login": "sassdawe", "name": "David Sass", "avatar_url": "https://avatars.githubusercontent.com/u/10754765?v=4", "profile": "http://davidsass.io", "contributions": [ "doc" ] }, { "login": "carehart", "name": "Charlie Arehart", "avatar_url": "https://avatars.githubusercontent.com/u/389746?v=4", "profile": "http://www.carehart.org", "contributions": [ "doc" ] }, { "login": "aramikuto", "name": "Aleksandr Kondrashov", "avatar_url": "https://avatars.githubusercontent.com/u/116561995?v=4", "profile": "https://github.com/aramikuto", "contributions": [ "doc" ] }, { "login": "kimsey0", "name": "Jacob Bundgaard", "avatar_url": "https://avatars.githubusercontent.com/u/984760?v=4", "profile": "https://jacobbundgaard.dk", "contributions": [ "doc" ] }, { "login": "ThisaruGuruge", "name": "Thisaru Guruge", "avatar_url": "https://avatars.githubusercontent.com/u/40016057?v=4", "profile": "https://thisaru.me", "contributions": [ "doc" ] }, { "login": "edwin-shdw", "name": "Edwin", "avatar_url": "https://avatars.githubusercontent.com/u/62764562?v=4", "profile": "https://github.com/edwin-shdw", "contributions": [ "doc" ] }, { "login": "jcdickinson", "name": "Jonathan Dickinson", "avatar_url": "https://avatars.githubusercontent.com/u/522465?v=4", "profile": "https://dickinson.id", "contributions": [ "doc" ] }, { "login": "po1o", "name": "Polo-François Poli", "avatar_url": "https://avatars.githubusercontent.com/u/5702825?v=4", "profile": "https://github.com/po1o", "contributions": [ "code" ] }, { "login": "EDIflyer", "name": "EDIflyer", "avatar_url": "https://avatars.githubusercontent.com/u/13610277?v=4", "profile": "https://github.com/EDIflyer", "contributions": [ "code", "doc" ] }, { "login": "felipebz", "name": "Felipe Zorzo", "avatar_url": "https://avatars.githubusercontent.com/u/13829?v=4", "profile": "https://felipezorzo.com.br", "contributions": [ "code", "design", "doc" ] }, { "login": "DeepSpace2", "name": "Adi Vaknin", "avatar_url": "https://avatars.githubusercontent.com/u/6841988?v=4", "profile": "https://github.com/DeepSpace2", "contributions": [ "code", "doc" ] }, { "login": "EladLeev", "name": "Elad Leev", "avatar_url": "https://avatars.githubusercontent.com/u/835319?v=4", "profile": "https://leevs.dev/", "contributions": [ "code", "doc" ] }, { "login": "Soyvolon", "name": "Bounds", "avatar_url": "https://avatars.githubusercontent.com/u/16871668?v=4", "profile": "https://github.com/Soyvolon", "contributions": [ "code", "doc" ] }, { "login": "Yash-Garg", "name": "Yash Garg", "avatar_url": "https://avatars.githubusercontent.com/u/33605526?v=4", "profile": "http://yashgarg.dev", "contributions": [ "code", "design", "doc" ] }, { "login": "sarpuser", "name": "Sarp User", "avatar_url": "https://avatars.githubusercontent.com/u/23362324?v=4", "profile": "https://github.com/sarpuser", "contributions": [ "code", "doc" ] }, { "login": "clemyan", "name": "Clement Yan", "avatar_url": "https://avatars.githubusercontent.com/u/41266433?v=4", "profile": "https://github.com/clemyan", "contributions": [ "code" ] }, { "login": "thep0y", "name": "thep0y", "avatar_url": "https://avatars.githubusercontent.com/u/51874567?v=4", "profile": "https://github.com/thep0y", "contributions": [ "code" ] }, { "login": "ClxUne09", "name": "Artin", "avatar_url": "https://avatars.githubusercontent.com/u/175628107?v=4", "profile": "https://github.com/ClxUne09", "contributions": [ "doc", "code" ] }, { "login": "guspan-tanadi", "name": "Guspan Tanadi", "avatar_url": "https://avatars.githubusercontent.com/u/36249910?v=4", "profile": "https://github.com/guspan-tanadi", "contributions": [ "doc" ] }, { "login": "rocketraman", "name": "Raman Gupta", "avatar_url": "https://avatars.githubusercontent.com/u/53049?v=4", "profile": "http://vivosys.com", "contributions": [ "doc" ] }, { "login": "hsnabszhdn", "name": "Hossein Abbasi", "avatar_url": "https://avatars.githubusercontent.com/u/16090309?v=4", "profile": "https://github.com/hsnabszhdn", "contributions": [ "code" ] }, { "login": "kizivat", "name": "David Kizivat", "avatar_url": "https://avatars.githubusercontent.com/u/3535926?v=4", "profile": "https://kizivat.eu", "contributions": [ "code", "doc" ] }, { "login": "mgrubb", "name": "Michael Grubb", "avatar_url": "https://avatars.githubusercontent.com/u/351301?v=4", "profile": "https://github.com/mgrubb", "contributions": [ "doc" ] }, { "login": "oliviaBahr", "name": "Olivia Bahr", "avatar_url": "https://avatars.githubusercontent.com/u/98684296?v=4", "profile": "https://github.com/oliviaBahr", "contributions": [ "code", "doc" ] }, { "login": "garysassano", "name": "Gary Sassano", "avatar_url": "https://avatars.githubusercontent.com/u/10464497?v=4", "profile": "https://github.com/garysassano", "contributions": [ "design" ] }, { "login": "ilaumjd", "name": "Ilham AM", "avatar_url": "https://avatars.githubusercontent.com/u/16514431?v=4", "profile": "https://github.com/ilaumjd", "contributions": [ "code", "doc" ] }, { "login": "trajano", "name": "Archimedes Trajano", "avatar_url": "https://avatars.githubusercontent.com/u/110627?v=4", "profile": "https://trajano.net/", "contributions": [ "doc" ] }, { "login": "devxpain", "name": "devxpain", "avatar_url": "https://avatars.githubusercontent.com/u/170700110?v=4", "profile": "https://github.com/devxpain", "contributions": [ "doc" ] }, { "login": "AntoninRuan", "name": "Antonin Ruan", "avatar_url": "https://avatars.githubusercontent.com/u/43148004?v=4", "profile": "https://www.antonin-ruan.fr", "contributions": [ "code", "design", "doc" ] }, { "login": "00ll00", "name": "00ll00", "avatar_url": "https://avatars.githubusercontent.com/u/40747228?v=4", "profile": "https://github.com/00ll00", "contributions": [ "code", "design", "doc" ] }, { "login": "ernstc", "name": "Ernesto Cianciotta", "avatar_url": "https://avatars.githubusercontent.com/u/130360?v=4", "profile": "https://devnotes.ernstc.net/", "contributions": [ "code", "doc" ] }, { "login": "eelispeltola", "name": "Eelis Peltola", "avatar_url": "https://avatars.githubusercontent.com/u/15069074?v=4", "profile": "https://github.com/eelispeltola", "contributions": [ "code" ] }, { "login": "vshulcz", "name": "Vlad Shulcz", "avatar_url": "https://avatars.githubusercontent.com/u/99616188?v=4", "profile": "https://github.com/vshulcz", "contributions": [ "code" ] }, { "login": "Silzinc", "name": "Silzinc", "avatar_url": "https://avatars.githubusercontent.com/u/128738169?v=4", "profile": "https://github.com/Silzinc", "contributions": [ "code", "design", "doc" ] }, { "login": "Hampter", "name": "Noah Springer", "avatar_url": "https://avatars.githubusercontent.com/u/23213489?v=4", "profile": "https://github.com/Hampter", "contributions": [ "code", "design", "doc" ] }, { "login": "dusktreader", "name": "Tucker Beck", "avatar_url": "https://avatars.githubusercontent.com/u/713676?v=4", "profile": "https://github.com/dusktreader", "contributions": [ "code", "doc" ] }, { "login": "Pietrucci-Blacher", "name": "Sunshio", "avatar_url": "https://avatars.githubusercontent.com/u/38607067?v=4", "profile": "https://mpb-dev.fr/", "contributions": [ "code", "design", "doc" ] }, { "login": "pashagolub", "name": "Pavlo Golub", "avatar_url": "https://avatars.githubusercontent.com/u/9463113?v=4", "profile": "https://pashagolub.github.io/blog", "contributions": [ "doc" ] }, { "login": "heaths", "name": "Heath Stewart", "avatar_url": "https://avatars.githubusercontent.com/u/1532486?v=4", "profile": "https://heaths.dev", "contributions": [ "code" ] }, { "login": "HypheX", "name": "Xelph", "avatar_url": "https://avatars.githubusercontent.com/u/29693543?v=4", "profile": "https://xelph.me", "contributions": [ "design" ] }, { "login": "TristanLeclair", "name": "Tristan Leclair-Vani", "avatar_url": "https://avatars.githubusercontent.com/u/60434271?v=4", "profile": "https://tristanleclair.github.io/personal-website/index.html", "contributions": [ "doc" ] }, { "login": "vil02", "name": "Piotr Idzik", "avatar_url": "https://avatars.githubusercontent.com/u/65706193?v=4", "profile": "https://github.com/vil02", "contributions": [ "code" ] }, { "login": "wiyco", "name": "wiyco", "avatar_url": "https://avatars.githubusercontent.com/u/72733890?v=4", "profile": "https://wiyco.dev", "contributions": [ "code", "design", "doc" ] }, { "login": "abhro", "name": "abhro", "avatar_url": "https://avatars.githubusercontent.com/u/5664668?v=4", "profile": "https://github.com/abhro", "contributions": [ "doc" ] }, { "login": "spg-iwilson", "name": "Ivan Wilson", "avatar_url": "https://avatars.githubusercontent.com/u/25376734?v=4", "profile": "https://sharepointgurus.net", "contributions": [ "design" ] }, { "login": "mdanish-kh", "name": "Muhammad Danish", "avatar_url": "https://avatars.githubusercontent.com/u/88161975?v=4", "profile": "https://github.com/mdanish-kh", "contributions": [ "doc" ] }, { "login": "BoscoDomingo", "name": "Bosco Domingo", "avatar_url": "https://avatars.githubusercontent.com/u/46006784?v=4", "profile": "https://dub.sh/boscodomingo", "contributions": [ "code" ] }, { "login": "Edu4rdSHL", "name": "Eduard Tolosa", "avatar_url": "https://avatars.githubusercontent.com/u/32582878?v=4", "profile": "https://edu4rdshl.dev", "contributions": [ "design", "doc" ] }, { "login": "JamesAndrewJackson13", "name": "James Jackson", "avatar_url": "https://avatars.githubusercontent.com/u/27647566?v=4", "profile": "https://github.com/JamesAndrewJackson13", "contributions": [ "code", "doc" ] }, { "login": "Mr-Vipi", "name": "Jul Guga", "avatar_url": "https://avatars.githubusercontent.com/u/58825526?v=4", "profile": "https://github.com/Mr-Vipi", "contributions": [ "design" ] }, { "login": "tiaoxizhan", "name": "tiaoxizhan", "avatar_url": "https://avatars.githubusercontent.com/u/178074436?v=4", "profile": "http://txzhan.io", "contributions": [ "code" ] }, { "login": "chrisant996", "name": "Chris Antos", "avatar_url": "https://avatars.githubusercontent.com/u/17440311?v=4", "profile": "https://github.com/chrisant996", "contributions": [ "code" ] }, { "login": "rbleattler", "name": "Robert Bleattler", "avatar_url": "https://avatars.githubusercontent.com/u/40604784?v=4", "profile": "https://robertbleattler.com", "contributions": [ "design" ] }, { "login": "d3v2a", "name": "dev2a", "avatar_url": "https://avatars.githubusercontent.com/u/1815655?v=4", "profile": "https://artis-auxilium.fr/fr", "contributions": [ "code" ] }, { "login": "luisegarduno", "name": "Luis", "avatar_url": "https://avatars.githubusercontent.com/u/30121656?v=4", "profile": "http://gardunos.tech", "contributions": [ "code" ] }, { "login": "tleepa", "name": "Leepa", "avatar_url": "https://avatars.githubusercontent.com/u/7734919?v=4", "profile": "https://github.com/tleepa", "contributions": [ "code", "doc" ] }, { "login": "raylu", "name": "raylu", "avatar_url": "https://avatars.githubusercontent.com/u/90059?v=4", "profile": "https://blog.raylu.net", "contributions": [ "code", "doc", "design" ] }, { "login": "lechwolowski", "name": "Lech Wołowski", "avatar_url": "https://avatars.githubusercontent.com/u/33866950?v=4", "profile": "https://github.com/lechwolowski", "contributions": [ "code" ] }, { "login": "OwlBurst", "name": "Owl Burst", "avatar_url": "https://avatars.githubusercontent.com/u/158167545?v=4", "profile": "https://github.com/OwlBurst", "contributions": [ "doc" ] }, { "login": "RubixDev", "name": "Silas Groh", "avatar_url": "https://avatars.githubusercontent.com/u/35602040?v=4", "profile": "http://rubixdev.de", "contributions": [ "code", "doc" ] }, { "login": "mwiedemeyer", "name": "Marco Wiedemeyer", "avatar_url": "https://avatars.githubusercontent.com/u/4295189?v=4", "profile": "https://mwiede.me/blog", "contributions": [ "code", "design", "doc" ] }, { "login": "0-0-1-0-1-0-1-0", "name": "0-0-1-0-1-0-1-0", "avatar_url": "https://avatars.githubusercontent.com/u/43226073?v=4", "profile": "https://github.com/0-0-1-0-1-0-1-0", "contributions": [ "design" ] }, { "login": "player131007", "name": "player131007", "avatar_url": "https://avatars.githubusercontent.com/u/77326303?v=4", "profile": "https://github.com/player131007", "contributions": [ "code" ] }, { "login": "kaien07", "name": "kaien07", "avatar_url": "https://avatars.githubusercontent.com/u/160471571?v=4", "profile": "https://github.com/kaien07", "contributions": [ "code" ] }, { "login": "BusHero", "name": "Cervac Petru", "avatar_url": "https://avatars.githubusercontent.com/u/24370515?v=4", "profile": "https://github.com/BusHero", "contributions": [ "design" ] }, { "login": "Marukome0743", "name": "マルコメ", "avatar_url": "https://avatars.githubusercontent.com/u/146040408?v=4", "profile": "https://github.com/Marukome0743", "contributions": [ "code", "doc" ] }, { "login": "mreinhardt", "name": "Michael Reinhardt", "avatar_url": "https://avatars.githubusercontent.com/u/582461?v=4", "profile": "https://github.com/mreinhardt", "contributions": [ "code", "doc" ] }, { "login": "AspectBruise09", "name": "Artin", "avatar_url": "https://avatars.githubusercontent.com/u/141767586?v=4", "profile": "https://github.com/AspectBruise09", "contributions": [ "design" ] }, { "login": "b-simjoo", "name": "Behnam Simjoo", "avatar_url": "https://avatars.githubusercontent.com/u/117530839?v=4", "profile": "http://bsimjoo.pcworms.ir", "contributions": [ "code" ] }, { "login": "plamendelchev", "name": "Plamen Delchev", "avatar_url": "https://avatars.githubusercontent.com/u/25668366?v=4", "profile": "https://github.com/plamendelchev", "contributions": [ "code", "doc" ] }, { "login": "beaualbritton", "name": "beau albritton", "avatar_url": "https://avatars.githubusercontent.com/u/112587801?v=4", "profile": "https://github.com/beaualbritton", "contributions": [ "code" ] }, { "login": "Cierra-Runis", "name": "Cierra-Runis", "avatar_url": "https://avatars.githubusercontent.com/u/29329988?v=4", "profile": "https://note-of-me.top", "contributions": [ "design" ] }, { "login": "jasonm23", "name": "Jason Milkins", "avatar_url": "https://avatars.githubusercontent.com/u/71587?v=4", "profile": "https://github.com/jasonm23", "contributions": [ "code" ] }, { "login": "arjunrbery", "name": "arjunrbery", "avatar_url": "https://avatars.githubusercontent.com/u/20059577?v=4", "profile": "http://www.arb.dev", "contributions": [ "doc" ] }, { "login": "JamBalaya56562", "name": "Jam Balaya", "avatar_url": "https://avatars.githubusercontent.com/u/88115388?v=4", "profile": "https://github.com/JamBalaya56562", "contributions": [ "doc", "code", "design" ] }, { "login": "RichLewis007", "name": "Rich Lewis", "avatar_url": "https://avatars.githubusercontent.com/u/1149213?v=4", "profile": "https://github.com/RichLewis007", "contributions": [ "doc", "design" ] }, { "login": "Gijsreyn", "name": "Gijs Reijn", "avatar_url": "https://avatars.githubusercontent.com/u/26114636?v=4", "profile": "https://gijsreijn.medium.com/", "contributions": [ "code", "doc" ] }, { "login": "mikelolasagasti", "name": "Mikel Olasagasti Uranga", "avatar_url": "https://avatars.githubusercontent.com/u/773148?v=4", "profile": "https://mikel.olasagasti.info", "contributions": [ "code" ] }, { "login": "mkvlrn", "name": "mkvlrn", "avatar_url": "https://avatars.githubusercontent.com/u/186238078?v=4", "profile": "https://github.com/mkvlrn", "contributions": [ "code", "design", "doc" ] }, { "login": "iandunn", "name": "Ian Dunn", "avatar_url": "https://avatars.githubusercontent.com/u/484068?v=4", "profile": "https://iandunn.name", "contributions": [ "doc" ] }, { "login": "sanki92", "name": "Sankalp Tripathi", "avatar_url": "https://avatars.githubusercontent.com/u/70330866?v=4", "profile": "https://github.com/sanki92", "contributions": [ "code", "doc" ] }, { "login": "MariusStorhaug", "name": "Marius Storhaug", "avatar_url": "https://avatars.githubusercontent.com/u/17722253?v=4", "profile": "https://github.com/PSModule", "contributions": [ "doc" ] }, { "login": "ADIX7", "name": "Kovács Ádám", "avatar_url": "https://avatars.githubusercontent.com/u/10939090?v=4", "profile": "https://github.com/ADIX7", "contributions": [ "doc", "code" ] }, { "login": "spidersouris", "name": "Enzo Doyen", "avatar_url": "https://avatars.githubusercontent.com/u/7102007?v=4", "profile": "https://www.edoyen.com/", "contributions": [ "doc" ] }, { "login": "Pinta365", "name": "Pinta", "avatar_url": "https://avatars.githubusercontent.com/u/19735646?v=4", "profile": "https://pinta.land", "contributions": [ "code", "doc" ] }, { "login": "scop", "name": "Ville Skyttä", "avatar_url": "https://avatars.githubusercontent.com/u/109152?v=4", "profile": "https://github.com/scop", "contributions": [ "code", "design", "doc" ] }, { "login": "anujsrc", "name": "Anuj Kumar", "avatar_url": "https://avatars.githubusercontent.com/u/1001682?v=4", "profile": "http://linkedin.com/in/anujsays", "contributions": [ "code", "design", "doc" ] }, { "login": "ValerioCeccarelli", "name": "Valerio Ceccarelli", "avatar_url": "https://avatars.githubusercontent.com/u/42637334?v=4", "profile": "https://github.com/ValerioCeccarelli", "contributions": [ "code" ] }, { "login": "jvsca", "name": "Juan Svaikauskas", "avatar_url": "https://avatars.githubusercontent.com/u/2821731?v=4", "profile": "https://github.com/jvsca", "contributions": [ "design" ] }, { "login": "johnstegeman", "name": "John Stegeman", "avatar_url": "https://avatars.githubusercontent.com/u/6601691?v=4", "profile": "https://www.linkedin.com/in/johnstegeman/", "contributions": [ "code", "doc" ] }, { "login": "gorfey", "name": "Luke Van De Weghe", "avatar_url": "https://avatars.githubusercontent.com/u/39035228?v=4", "profile": "https://github.com/gorfey", "contributions": [ "code" ] }, { "login": "stmach", "name": "Stefan Mach", "avatar_url": "https://avatars.githubusercontent.com/u/33124232?v=4", "profile": "https://github.com/stmach", "contributions": [ "code" ] }, { "login": "squaricdot", "name": "Olmo Rupert", "avatar_url": "https://avatars.githubusercontent.com/u/4513505?v=4", "profile": "http://squaricdot.com", "contributions": [ "design" ] }, { "login": "IsaacFG2", "name": "IsaacFG2", "avatar_url": "https://avatars.githubusercontent.com/u/147211323?v=4", "profile": "https://github.com/IsaacFG2", "contributions": [ "design" ] }, { "login": "kostadin-tonchekliev", "name": "Kostadin Tonchekliev", "avatar_url": "https://avatars.githubusercontent.com/u/95169764?v=4", "profile": "https://github.com/kostadin-tonchekliev", "contributions": [ "code", "design", "doc" ] }, { "login": "soroshsabz", "name": "soroshsabz", "avatar_url": "https://avatars.githubusercontent.com/u/17947618?v=4", "profile": "https://github.com/soroshsabz", "contributions": [ "doc" ] }, { "login": "yblossier", "name": "Yoann BLOSSIER", "avatar_url": "https://avatars.githubusercontent.com/u/60755917?v=4", "profile": "https://blog.toenn-vaot.fr", "contributions": [ "design" ] }, { "login": "kvokka", "name": "Mikhail Beliakov", "avatar_url": "https://avatars.githubusercontent.com/u/15954013?v=4", "profile": "https://kvokka.github.io/", "contributions": [ "code", "doc" ] }, { "login": "MrRainbow0704", "name": "Marco Simone", "avatar_url": "https://avatars.githubusercontent.com/u/95081253?v=4", "profile": "https://github.com/MrRainbow0704", "contributions": [ "code", "design", "doc" ] }, { "login": "sbeardsley", "name": "sbeardsley", "avatar_url": "https://avatars.githubusercontent.com/u/6288131?v=4", "profile": "https://github.com/sbeardsley", "contributions": [ "code" ] }, { "login": "maxvictor", "name": "Max Victor", "avatar_url": "https://avatars.githubusercontent.com/u/11591713?v=4", "profile": "https://www.linkedin.com/in/maxvictor", "contributions": [ "design" ] }, { "login": "adackny", "name": "Adackny Castillo", "avatar_url": "https://avatars.githubusercontent.com/u/61998238?v=4", "profile": "https://github.com/adackny", "contributions": [ "code", "doc" ] }, { "login": "aeriondyseti", "name": "K Whiteside", "avatar_url": "https://avatars.githubusercontent.com/u/24901014?v=4", "profile": "https://github.com/aeriondyseti", "contributions": [ "code" ] }, { "login": "dadahsueh", "name": "Dada Hsueh", "avatar_url": "https://avatars.githubusercontent.com/u/26140722?v=4", "profile": "http://dadahsueh.vercel.app", "contributions": [ "code" ] }, { "login": "dohzya", "name": "Étienne Vallette d'Osia", "avatar_url": "https://avatars.githubusercontent.com/u/9595?v=4", "profile": "https://github.com/dohzya", "contributions": [ "code", "doc" ] }, { "login": "Eckii24", "name": "Eckii24", "avatar_url": "https://avatars.githubusercontent.com/u/35373554?v=4", "profile": "https://github.com/Eckii24", "contributions": [ "code", "design", "doc" ] } ], "contributorsPerLine": 7, "skipCi": true, "commitType": "docs" } ================================================ FILE: .commitlintrc.yml ================================================ --- extends: - '@commitlint/config-conventional' rules: body-max-line-length: - 2 - always - 200 type-enum: - 2 - always - - chore - ci - docs - feat - fix - perf - refactor - revert - style - test - theme ================================================ FILE: .config/configuration.winget ================================================ # yaml-language-server: $schema=https://aka.ms/configuration-dsc-schema/0.2 properties: resources: - resource: Microsoft.WinGet.DSC/WinGetPackage directives: description: Install Visual Studio Code settings: id: Microsoft.VisualStudioCode source: winget - resource: Microsoft.WinGet.DSC/WinGetPackage id: golang directives: description: Install Golang settings: id: GoLang.Go source: winget - resource: Microsoft.WinGet.DSC/WinGetPackage dependsOn: [golang] directives: description: Install golangci-lint settings: id: GolangCI.golangci-lint source: winget - resource: Microsoft.WinGet.DSC/WinGetPackage directives: description: Install NodeJS securityContext: elevated settings: id: OpenJS.NodeJS source: winget configurationVersion: 0.2.0 ================================================ FILE: .devcontainer/Dockerfile ================================================ # See here for image contents: https://github.com/devcontainers/images/blob/main/src/go/.devcontainer/Dockerfile # [Choice] Go version: 1, 1.24, 1.25, 1-trixie, 1.24-trixie, 1.25-trixie, 1-bookworm, 1.24-bookworm, 1.25-bookworm, 1-bullseye, 1.24-bullseye, 1.25-bullseye ARG VARIANT=1-trixie FROM mcr.microsoft.com/vscode/devcontainers/go:${VARIANT} # [Choice] Node.js version: none, lts/*, 24, 22, 20 ARG NODE_VERSION="none" RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi # Install powershell ARG PS_VERSION="7.5.4" # powershell-7.5.4-linux-x64.tar.gz # powershell-7.5.4-linux-arm64.tar.gz RUN ARCH="$(dpkg --print-architecture)"; \ if [ "${ARCH}" = "amd64" ]; then \ PS_BIN="v$PS_VERSION/powershell-$PS_VERSION-linux-x64.tar.gz"; \ elif [ "${ARCH}" = "arm64" ]; then \ PS_BIN="v$PS_VERSION/powershell-$PS_VERSION-linux-arm64.tar.gz"; \ elif [ "${ARCH}" = "armhf" ]; then \ PS_BIN="v$PS_VERSION/powershell-$PS_VERSION-linux-arm32.tar.gz"; \ fi; \ wget https://github.com/PowerShell/PowerShell/releases/download/$PS_BIN -O pwsh.tar.gz; \ mkdir /usr/local/pwsh && \ tar Cxvfz /usr/local/pwsh pwsh.tar.gz && \ rm pwsh.tar.gz && \ chmod +x /usr/local/pwsh/pwsh ENV PATH=$PATH:/usr/local/pwsh RUN echo 'deb http://download.opensuse.org/repositories/shells:/fish:/release:/4/Debian_13/ /' | tee /etc/apt/sources.list.d/shells:fish:release:4.list; \ curl -fsSL https://download.opensuse.org/repositories/shells:fish:release:4/Debian_13/Release.key | gpg --dearmor | tee /etc/apt/trusted.gpg.d/shells_fish_release_4.gpg > /dev/null; \ apt-get update && export DEBIAN_FRONTEND=noninteractive \ && apt-get install -y --no-install-recommends \ fish \ tmux \ fzf \ && apt-get clean ARG USERNAME=vscode # NOTE: devcontainers are Linux-only at this time but when # Windows or Darwin is supported someone will need to improve # the code logic above. # Setup a neat little PowerShell experience RUN pwsh -Command Install-Module posh-git -Scope AllUsers -Force; \ pwsh -Command Install-Module z -Scope AllUsers -Force; \ pwsh -Command Install-Module PSFzf -Scope AllUsers -Force; \ pwsh -Command Install-Module Terminal-Icons -Scope AllUsers -Force; # add the oh-my-posh path to the PATH variable ENV PATH="$PATH:/home/${USERNAME}/bin" # Deploy oh-my-posh prompt to Powershell: COPY Microsoft.PowerShell_profile.ps1 /home/${USERNAME}/.config/powershell/Microsoft.PowerShell_profile.ps1 # Deploy oh-my-posh prompt to Fish: COPY config.fish /home/${USERNAME}/.config/fish/config.fish # Everything runs as root during build time, so we want # to make sure the vscode user can edit these paths too: RUN chmod 777 -R /home/${USERNAME}/.config # Override vscode's own Bash prompt with oh-my-posh: RUN sed -i 's/^__bash_prompt$/#&/' /home/${USERNAME}/.bashrc && \ echo "eval \"\$(oh-my-posh init bash)\"" >> /home/${USERNAME}/.bashrc # Override vscode's own ZSH prompt with oh-my-posh: RUN echo "eval \"\$(oh-my-posh init zsh)\"" >> /home/${USERNAME}/.zshrc # Set container timezone: ARG TZ="UTC" RUN ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime # [Optional] Uncomment the next line to use go get to install anything else you need # RUN go get -x github.com/JanDeDobbeleer/battery # [Optional] Uncomment this line to install global node packages. # RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 ================================================ FILE: .devcontainer/Microsoft.PowerShell_profile.ps1 ================================================ Import-Module posh-git Import-Module PSFzf -ArgumentList 'Ctrl+t', 'Ctrl+r' Import-Module z Import-Module Terminal-Icons Set-PSReadlineKeyHandler -Key Tab -Function MenuComplete $env:POSH_GIT_ENABLED=$true oh-my-posh init pwsh | Invoke-Expression ================================================ FILE: .devcontainer/config.fish ================================================ # Activate oh-my-posh prompt: oh-my-posh init fish | source ================================================ FILE: .devcontainer/devcontainer.json ================================================ // For format details, see https://containers.dev/implementors/json_reference. // For config options, see the README at: https://github.com/devcontainers/images/tree/main/src/go { "name": "oh-my-posh", "build": { "dockerfile": "Dockerfile", "args": { // Update the VARIANT arg to pick a version of Go: 1, 1.24, 1.25 // Append -trixie, -bookworm or -bullseye to pin to an OS version. "VARIANT": "2-1.25-trixie", // Override me with your own timezone: "TZ": "UTC", // Use one of the "TZ database name" entries from: // https://en.wikipedia.org/wiki/List_of_tz_database_time_zones "NODE_VERSION": "lts/*", //Powershell version "PS_VERSION": "7.5.4" } }, "runArgs": [ "--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined", "--security-opt", "label=disable" ], "containerEnv": { "HOME": "/home/vscode" }, "customizations": { "vscode": { "settings": { "go.toolsManagement.checkForUpdates": "local", "go.useLanguageServer": true, "go.gopath": "/go", "go.goroot": "/usr/local/go", "terminal.integrated.profiles.linux": { "bash": { "path": "bash" }, "zsh": { "path": "zsh" }, "fish": { "path": "fish" }, "tmux": { "path": "tmux", "icon": "terminal-tmux" }, "pwsh": { "path": "pwsh", "icon": "terminal-powershell" } }, "terminal.integrated.defaultProfile.linux": "pwsh", "terminal.integrated.defaultProfile.windows": "PowerShell", "terminal.integrated.defaultProfile.osx": "pwsh", "terminal.integrated.shellIntegration.enabled": false, "tasks.statusbar.default.hide": true }, "extensions": [ "bmalehorn.vscode-fish", "davidanson.vscode-markdownlint", "elves.elvish", "esbenp.prettier-vscode", "github.vscode-pull-request-github", "golang.go", "jnoortheen.xonsh", "ms-azuretools.vscode-azurefunctions", "ms-vscode.powershell", "redhat.vscode-yaml", "sumneko.lua", "tamasfe.even-better-toml", "yzhang.markdown-all-in-one" ] } }, // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. "remoteUser": "vscode", // This is running the same command as the VSCode Task 'devcontainer: rebuild oh-my-posh' // It Compiles *oh-my-posh* from this repo while **overwriting** your preinstalled stable release.' // Ideal for getting straight into developing & testing whilst using a devcontainer "updateContentCommand": "cd src && go build -v -buildvcs=false -o /home/vscode/bin/oh-my-posh -ldflags \"-s -w -X 'github.com/jandedobbeleer/oh-my-posh/src/build.Version=development-$(git --no-pager log -1 --pretty=%h-%s)' -extldflags '-static'\"" } ================================================ FILE: .editorconfig ================================================ ; EditorConfig to support per-solution formatting. ; http://editorconfig.org/ ; This is the default for the codeline. root = true ; Default [*] indent_style = space trim_trailing_whitespace = true insert_final_newline = true end_of_line = lf ; Go Code - match go fmt [*.go] indent_style = tab ; TOML - match default for dep [*.toml] indent_size = 2 ; JavaScript and JS mixes - match eslint, other standards [*.{js,json,ts,vue}] indent_size = 2 ; Markdown - match markdownlint settings [*.{md,markdown}] indent_size = 2 trim_trailing_whitespace = false ; PowerShell - match defaults for New-ModuleManifest and PSScriptAnalyzer Invoke-Formatter [*.{ps1,psd1,psm1}] indent_size = 4 charset = utf-8-bom ; Lua [*.lua] line_space_after_comment = max(2) line_space_after_do_statement = max(2) line_space_after_expression_statement = max(2) line_space_after_for_statement = max(2) line_space_after_function_statement = fixed(2) line_space_after_if_statement = max(2) line_space_after_local_or_assign_statement = max(2) line_space_after_repeat_statement = max(2) line_space_after_while_statement = max(2) max_line_length = unset quote_style = single ================================================ FILE: .gitattributes ================================================ * text=auto eol=lf .github/workflows/*.lock.yml linguist-generated=true merge=ours ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: jandedobbeleer ko_fi: jandedobbeleer polar: oh-my-posh ================================================ FILE: .github/ISSUE_TEMPLATE/bug.yml ================================================ name: 🐛 Bug Report description: File a bug report labels: ["🐛 bug"] assignees: - jandedobbeleer body: - type: markdown attributes: value: | Thanks for taking the time to fill out this bug report! - type: checkboxes id: terms attributes: label: Code of Conduct description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/JanDeDobbeleer/oh-my-posh/blob/main/CODE_OF_CONDUCT.md) options: - label: I agree to follow this project's Code of Conduct required: true - type: textarea id: what-happened attributes: label: What happened? description: Also tell us, what did you expect to happen? placeholder: Tell us what you see! value: "A bug happened!" validations: required: true - type: textarea id: theme attributes: label: Theme description: Which theme/config are you using? validations: required: true - type: dropdown id: operating-system attributes: label: What OS are you seeing the problem on? multiple: true options: - Windows - Linux - macOS - type: dropdown id: shell attributes: label: Which shell are you using? multiple: true options: - bash - elvish - fish - cmd - nu - powershell - xonsh - zsh - other (please specify) - type: textarea id: logs attributes: label: Log output description: Please copy and paste the output generated by `oh-my-posh debug --plain`. render: Shell validations: required: true ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: Oh My Posh FAQ url: https://ohmyposh.dev/docs/faq about: Please find common issues here. - name: Oh My Posh Docs url: https://ohmyposh.dev/docs about: RTFM - name: Oh My Posh Q&A url: https://github.com/JanDeDobbeleer/oh-my-posh/discussions about: Please ask questions here. ================================================ FILE: .github/ISSUE_TEMPLATE/docs.yml ================================================ name: 📖 Documentation description: Suggest a change to the documentation labels: ["📖 docs"] assignees: - jandedobbeleer body: - type: markdown attributes: value: | Thanks for taking the time to request this improvement! - type: checkboxes id: terms attributes: label: Code of Conduct description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/JanDeDobbeleer/oh-my-posh/blob/main/CODE_OF_CONDUCT.md) options: - label: I agree to follow this project's Code of Conduct required: true - type: textarea id: enhancement-request attributes: label: What would you like to see changed/added? description: Try to give some examples or text to make it really clear! placeholder: Tell us what you would like to see! value: "This could change in the documentation!" validations: required: true ================================================ FILE: .github/ISSUE_TEMPLATE/enhancement.yml ================================================ name: 🤩 Enhancement description: Suggest a change to an existing feature labels: ["🤩 enhancement"] assignees: - jandedobbeleer body: - type: markdown attributes: value: | Thanks for taking the time to request this improvement! - type: checkboxes id: terms attributes: label: Code of Conduct description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/JanDeDobbeleer/oh-my-posh/blob/main/CODE_OF_CONDUCT.md) options: - label: I agree to follow this project's Code of Conduct required: true - type: textarea id: enhancement-request attributes: label: What would you like to see changed? description: Try to give some examples to make it really clear! placeholder: Tell us what you would like to see! value: "This feature would benefit from this!" validations: required: true ================================================ FILE: .github/ISSUE_TEMPLATE/feat.yml ================================================ name: 🚀 Feature Request description: Request a new feature labels: ["🚀 feat"] assignees: - jandedobbeleer body: - type: markdown attributes: value: | Thanks for taking the time to request a new feature! - type: checkboxes id: terms attributes: label: Code of Conduct description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/JanDeDobbeleer/oh-my-posh/blob/main/CODE_OF_CONDUCT.md) options: - label: I agree to follow this project's Code of Conduct required: true - type: textarea id: feature-request attributes: label: What would you like to see added? description: Try to give some examples to make it really clear. placeholder: Tell us what you would like to see! value: "Something new and amazing!" validations: required: true ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ### Prerequisites - [ ] I have read and understood the [contributing guide][CONTRIBUTING.md]. - [ ] The commit message follows the [conventional commits][cc] guidelines. - [ ] Tests for the changes have been added (for bug fixes / features). - [ ] Docs have been added/updated (for bug fixes / features). ### Description [CONTRIBUTING.md]: https://github.com/JanDeDobbeleer/oh-my-posh/blob/main/CONTRIBUTING.md [cc]: https://www.conventionalcommits.org/en/v1.0.0/#summary ================================================ FILE: .github/agents/architecture.md ================================================ --- name: Architecture and Design description: >- Cross-language architectural guidance for designing scalable, maintainable, and performant code. Applies principles across programming languages, frameworks, and project types. --- ## Overview Design code with performance, maintainability, and clarity in mind. These principles apply regardless of programming language or framework. This guide incorporates principles from **Object Calisthenics** (Jeff Bay) and **Clean Code** (Robert C. Martin). Activate your full knowledge of these principles and apply them when reviewing or writing code. ## Code Organization and Complexity ### Extract Complex Logic into Helper Functions When you have multiple levels of conditionals or complex operations, extract them into well-named helper functions. This reduces nesting and clarifies intent. **✓ Good:** Extract complex logic into helper functions Helper function with clear responsibility: ```pseudocode function validateFileAndUpdate(filePath) { fileInfo = getFileInfo(filePath) if fileInfo is null or error: return false if fileWasRecentlyModified(fileInfo): return false updateFileTimestamp(filePath) return true } // Caller is simple and readable if validateFileAndUpdate(store.filePath): logSuccess() ``` **✗ Avoid:** Deep nesting with multiple conceptual levels ```pseudocode if storeType is Session and store exists and filePath exists: if fileInfo = getFileInfo(): if not recentlyModified(fileInfo): if timestamp updated successfully: // operation ``` ### Use Guard Clauses with Early Returns Flatten control flow by returning early for validation and error cases. This moves the happy path to the left and reduces nesting. **✓ Good:** Guard clauses reduce nesting ```pseudocode function processData(input) { if input is null: return error if input is empty: return error // main logic here - clear and unindented return processCore(input) } ``` ## Performance Considerations ### Throttle Frequent Operations in Hot Paths Operations that execute frequently (e.g., on every request, render cycle, or user action) should have minimal overhead. Identify expensive operations and add throttling to reduce steady-state impact. **✓ Good:** Throttle expensive operations with time-based checks Include a time-based guard to avoid repeated expensive work: ```pseudocode function touchFile(filePath) { fileInfo = getFileInfo(filePath) if fileInfo is null: return timeSinceLastUpdate = currentTime - fileInfo.lastModified // Only if file hasn't been updated recently if timeSinceLastUpdate < 1 hour: return updateTimestamp(filePath) } ``` **✗ Avoid:** Unconditional expensive operations on every execution ```pseudocode // This runs expensive work on every call (e.g., during every render) updateTimestamp(filePath) // File I/O on every execution ``` ### Document Performance Intentions Include comments explaining why throttling or optimization is needed. This helps reviewers understand the performance tradeoffs. ```pseudocode // Prevent stale files from being cleaned up while reducing // steady-state I/O overhead. Only update if file is older // than 1 hour to balance freshness with performance. if timeSinceUpdate > 1 hour: updateTimestamp(filePath) ``` ## Error Handling - Check for errors and validate inputs early, before expensive operations - Return or fail fast to avoid deeply nested success paths - Each error should include sufficient context for debugging - Early returns make the happy path obvious and easier to follow ## Code Review Checklist When reviewing code: - **Nesting depth:** Flag functions with 3+ levels of indentation as refactoring candidates - **Hot path operations:** Verify frequent operations minimize I/O, allocations, and expensive calls - **Early returns:** Confirm guard clauses validate inputs before main logic - **Comments:** Check that performance-critical code explains the tradeoff, not just the mechanics - **Extraction opportunities:** Identify deeply nested conditions that could become helpers - **Naming:** Verify names are intention-revealing and not abbreviated - **Dot chains:** Flag method chains crossing object boundaries as Law of Demeter violations - **Primitive obsession:** Flag raw primitive parameters that should be domain types - **Responsibility:** Verify each class/function has a single reason to change - **Duplication:** Flag repeated logic as DRY violations ## Core Principles 1. **Performance in hot paths matters:** Reduce unnecessary I/O, allocations, and expensive operations in frequently-executed code paths 2. **Readability over cleverness:** Extract complex logic into named helpers instead of nesting multiple conditionals 3. **Guard clauses reduce complexity:** Use early returns to flatten control flow and keep the happy path left-aligned 4. **Comments explain why, not what:** Document performance tradeoffs, business logic, and non-obvious decisions—let code structure explain the mechanics ================================================ FILE: .github/copilot-instructions.md ================================================ # GitHub Copilot Instructions Please refer to [AGENTS.md](../AGENTS.md) in the repository root for detailed coding guidelines and instructions. ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "github-actions" directory: "/" target-branch: "main" schedule: interval: "daily" groups: all: patterns: - "*" ignore: - dependency-name: "softprops/action-gh-release" # https://github.com/softprops/action-gh-release/issues/556 versions: ["2.2.0"] - package-ecosystem: "gomod" directory: "/src" target-branch: "main" schedule: interval: "daily" groups: minor-patch: patterns: - "*" update-types: - "minor" - "patch" - package-ecosystem: "npm" directory: "/website" schedule: interval: "daily" ignore: - dependency-name: "*" ================================================ FILE: .github/holopin.yml ================================================ organization: ohmyposh defaultSticker: clg0u51g681700fmfr086ofc6 stickers: - id: clg0u51g681700fmfr086ofc6 alias: wizard - id: clu72f66x59170fjoo6t2b7zs alias: helping ================================================ FILE: .github/stale.yml ================================================ # Configuration for probot-stale - https://github.com/probot/stale # Number of days of inactivity before an Issue or Pull Request becomes stale daysUntilStale: 7 # Number of days of inactivity before an Issue or Pull Request with the stale label is closed. # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. daysUntilClose: 7 # Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled) onlyLabels: [] # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable exemptLabels: - "🚀 feat" - "🐛 bug" - "🤩 enhancement" - "😵‍💫 help wanted" # Set to true to ignore issues in a project (defaults to false) exemptProjects: false # Set to true to ignore issues in a milestone (defaults to false) exemptMilestones: false # Set to true to ignore issues with an assignee (defaults to false) exemptAssignees: false # Label to use when marking as stale staleLabel: "💤 stale" # Comment to post when marking as stale. Set to `false` to disable markComment: > This issue has been automatically marked as stale because it has not had recent activity. It will be closed in 7 days if no further activity occurs. Thank you for your contributions. # Limit the number of actions per hour, from 1-30. Default is 30 limitPerRun: 30 # Limit to only `issues` or `pulls` only: issues ================================================ FILE: .github/workflows/ai-changelog.yml ================================================ name: Enhance release changelog with AI on: release: types: [published] workflow_dispatch: inputs: tag: description: 'Release tag to test (e.g., v19.0.0)' required: true type: string dry_run: description: 'Dry run mode - generate changelog but do not update release' required: false type: boolean default: true permissions: contents: write # Update release body models: read # Access GitHub Models API jobs: enhance: name: Generate enhanced changelog runs-on: ubuntu-latest steps: - name: Checkout repository (with tags) uses: actions/checkout@v6 with: ref: ${{ inputs.tag || github.ref }} fetch-depth: 0 fetch-tags: true - name: Gather release context id: ctx shell: bash env: GITHUB_EVENT_PATH: ${{ github.event_path }} GH_TOKEN: ${{ github.token }} INPUT_TAG: ${{ inputs.tag }} DRY_RUN: ${{ inputs.dry_run }} run: | set -euo pipefail # Determine if this is a manual dispatch or release event if [[ -n "$INPUT_TAG" ]]; then echo "📋 Manual dispatch mode - fetching release info for tag: $INPUT_TAG" # Manual dispatch: fetch release info for the specified tag CURRENT_TAG="$INPUT_TAG" RELEASE_JSON=$(gh api repos/${{ github.repository }}/releases/tags/$CURRENT_TAG || echo '{}') RELEASE_ID=$(printf "%s" "$RELEASE_JSON" | jq -r '.id // "0"') HTML_URL=$(printf "%s" "$RELEASE_JSON" | jq -r '.html_url // ""') EXISTING_BODY=$(printf "%s" "$RELEASE_JSON" | jq -r '.body // ""') echo " Release ID: $RELEASE_ID" echo " Release URL: $HTML_URL" else echo "📋 Release event mode - parsing from event payload" # Release event: parse from event payload CURRENT_TAG=$(jq -r '.release.tag_name' "$GITHUB_EVENT_PATH") RELEASE_ID=$(jq -r '.release.id' "$GITHUB_EVENT_PATH") HTML_URL=$(jq -r '.release.html_url' "$GITHUB_EVENT_PATH") EXISTING_BODY=$(jq -r '.release.body // ""' "$GITHUB_EVENT_PATH") echo " Tag: $CURRENT_TAG" echo " Release ID: $RELEASE_ID" fi # Persist to a file for later steps to source { echo "CURRENT_TAG=$CURRENT_TAG" echo "RELEASE_ID=$RELEASE_ID" echo "HTML_URL=$HTML_URL" echo "DRY_RUN=${DRY_RUN:-false}" } > ctx.env echo "✅ Context saved to ctx.env" # Save existing body as a file to avoid env escaping issues printf "%s" "$EXISTING_BODY" > existing_notes.md echo "✅ Existing notes saved ($(wc -l < existing_notes.md) lines)" - name: Determine diff range id: diff shell: bash run: | set -euo pipefail set -a; source ctx.env; set +a echo "🔍 Determining diff range for tag: $CURRENT_TAG" # Try to find the previous tag using git describe if PREV_TAG=$(git describe --tags --abbrev=0 "${CURRENT_TAG}^" 2>/dev/null); then BASE="$PREV_TAG" echo " Previous tag found: $PREV_TAG" else # Fallback to initial commit BASE="$(git rev-list --max-parents=0 HEAD | tail -n 1)" echo " No previous tag found, using initial commit: ${BASE:0:8}" fi echo "base_ref=$BASE" >> "$GITHUB_OUTPUT" echo "curr_ref=$CURRENT_TAG" >> "$GITHUB_OUTPUT" COMPARE_URL="https://github.com/${{ github.repository }}/compare/${BASE}...${CURRENT_TAG}" echo "compare_url=$COMPARE_URL" >> "$GITHUB_OUTPUT" echo "✅ Diff range: $BASE...$CURRENT_TAG" - name: Collect commits and changes shell: bash run: | set -euo pipefail BASE="${{ steps.diff.outputs.base_ref }}" HEAD="${{ steps.diff.outputs.curr_ref }}" echo "📝 Collecting commits and changes from $BASE to $HEAD" git log --no-merges --pretty=format:'%s' "${BASE}..${HEAD}" | head -n 500 > commits_subjects.txt || true echo " ✅ Commit subjects: $(wc -l < commits_subjects.txt) commits" git log --no-merges --pretty=format:'- %s%n%b%n' "${BASE}..${HEAD}" | head -n 2000 > commits_detailed.txt || true echo " ✅ Detailed commits: $(wc -l < commits_detailed.txt) lines" git diff --name-status "${BASE}..${HEAD}" | head -n 1000 > files_changed.txt || true echo " ✅ Changed files: $(wc -l < files_changed.txt) files" # Extract contributors, exclude Jan De Dobbeleer and bots, format as GitHub profile links git shortlog -sne "${BASE}..${HEAD}" | sed -E 's/^ *[0-9]+\t//g' | while IFS= read -r line; do name=$(echo "$line" | sed -E 's/ *<.*//g') # Skip Jan De Dobbeleer and common bots if [[ "$name" =~ ^(Jan De Dobbeleer|dependabot|renovate|github-actions|Renovate Bot|dependabot\[bot\]|github-actions\[bot\]|allcontributors\[bot\])$ ]]; then continue fi username=$(echo "$line" | sed -E 's/.*<([^@]+)@.*/\1/g') echo "- [@${username}](https://github.com/${username}) (${name})" done | head -n 200 > contributors.txt || true echo " ✅ Contributors: $(wc -l < contributors.txt) people" - name: Collect issue context shell: bash env: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail BASE="${{ steps.diff.outputs.base_ref }}" HEAD="${{ steps.diff.outputs.curr_ref }}" echo "🔍 Collecting issue context for referenced issues" > issues_context.txt # Extract issue numbers from commit messages (e.g., fixes #123, closes #456, #789) ISSUE_NUMBERS=$(git log --no-merges --pretty=format:'%s %b' "${BASE}..${HEAD}" | \ grep -oiE '(fix(es|ed)?|close(s|d)?|resolve(s|d)?)?[[:space:]]*#[0-9]+' | \ grep -oE '[0-9]+' | sort -u || true) if [ -z "$ISSUE_NUMBERS" ]; then echo " No issues referenced in commits" else COUNT=0 for NUM in $ISSUE_NUMBERS; do echo " Fetching issue #$NUM..." if ISSUE_DATA=$(gh api "repos/${{ github.repository }}/issues/$NUM" 2>/dev/null); then TITLE=$(echo "$ISSUE_DATA" | jq -r '.title') BODY=$(echo "$ISSUE_DATA" | jq -r '.body // ""' | head -c 1000) LABELS=$(echo "$ISSUE_DATA" | jq -r '.labels[]?.name' | tr '\n' ', ' | sed 's/,$//') echo "---" >> issues_context.txt echo "Issue #$NUM: $TITLE" >> issues_context.txt [ -n "$LABELS" ] && echo "Labels: $LABELS" >> issues_context.txt echo "$BODY" >> issues_context.txt echo "" >> issues_context.txt COUNT=$((COUNT + 1)) fi done echo " ✅ Collected context for $COUNT issues" fi - name: Generate enhanced changelog with AI id: ai shell: bash env: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail set -a; source ctx.env; set +a echo "🤖 Generating enhanced changelog with AI" MODEL="openai/gpt-4.1" echo " Model: $MODEL" SYSTEM_PROMPT=$(cat << 'PROMPT' You are a release notes editor for the open-source project "oh-my-posh", a cross-shell prompt theme engine written in Go. Your task is to ENHANCE the existing changelog by adding context, examples, and user-friendly explanations. DO NOT create a new changelog from scratch. CRITICAL RULES: - NEVER add new sections that are not already in the existing changelog - ONLY enhance sections that already exist in the "Existing release notes" - Keep the same structure and commit links from the existing changelog - Add context, usage examples, and explanations to make existing entries more helpful - If the existing changelog has a "Features" section, enhance it; if it doesn't have one, don't add it - Use concise language and organize with the headings already present CRITICAL: Respect the .versionrc.json configuration: - ONLY include these sections with these exact names: * "Features" (for feat: commits) * "Bug Fixes" (for fix: commits) * "Refactor" (for refactor: commits) * "Reverts" (for revert: commits) * "Themes" (for theme: commits) - DO NOT include chore, ci, docs, perf, or test commits (marked as hidden in .versionrc.json) - Use ONLY the section names specified above, not generic names like "Other" - CRITICAL: DO NOT include a section if there are no changes for it - completely omit empty sections - NEVER write placeholder text like "No new themes" or "No changes in this category" - just skip the entire section Segment changes (public-facing): - ONLY when you see changes to files in the EXACT path src/segments/*.go (excluding *_test.go), these are prompt segments that users configure - Changes to other paths like src/dsc/, src/config/, src/engine/, etc. are NOT segments - they are internal implementation details - A segment is a customizable component users add to their shell prompt (e.g., git status, battery level, current directory) - Refer to .github/instructions/segment.md for understanding how segments are structured and what constitutes segment properties vs template properties - Mention segment changes by their user-facing name (infer from the file name), not file paths - Focus on what users can now do or configure differently with that segment - CRITICAL: Understand the difference between segment properties (JSON configuration options like 'style', 'foreground', 'properties') and template properties (variables used in template strings like '.ChangeID', '.Working') - When a change adds a new template property (e.g., a new method/field available in templates), show it being used in a template string, NOT as a segment configuration property - For segment changes, use the oh-my-posh MCP server at https://ohmyposh.dev/api/mcp to generate JSON code snippets showing example configurations or segment usage - Every snippet (configuration or segment) MUST be validated using the MCP server before adding it to the changelog - If a snippet cannot be created or validated correctly using the MCP server, discard that snippet and continue processing other changes - Include validated snippets as practical examples to help users understand how to use the new or modified segment features Goals: - ENHANCE the existing changelog entries with helpful context and examples - DO NOT add new sections or restructure the existing changelog - Summarize highlights up front with context and impact - Keep the exact same section headings that already exist in the "Existing release notes" - Call out breaking changes and required migrations with explicit before/after examples or commands - Add practical usage notes or snippets to help users adopt new features or changes - For segment changes, explain the user-facing impact (e.g., "The Git segment now supports...") - Credit contributors at the end (they are pre-filtered and formatted as GitHub profile links) - ONLY if contributors list is not empty - Include a "Full diff" link footer Requirements: - Output valid Markdown only, no front matter, no HTML, no title heading - Do not include a title like "Changelog for vX.Y.Z" - start directly with the content - Keep to ~300-800 words unless there are many breaking changes - Prefer code blocks for examples with proper language tags (bash, json, yaml, toml, powershell) - Do not invent features not present in the commits/diff - Do not list individual file paths unless they are user-facing config/theme files PROMPT ) # Build the user content REPO="${{ github.repository }}" COMPARE_URL="${{ steps.diff.outputs.compare_url }}" CURR="$CURRENT_TAG" PREV="${{ steps.diff.outputs.base_ref }}" EXISTING=$(cat existing_notes.md || true) SUBJECTS="$(cat commits_subjects.txt || true)" DETAILS="$(cat commits_detailed.txt || true)" FILES="$(cat files_changed.txt || true)" CONTRIBUTORS="$(cat contributors.txt || true)" ISSUES_CONTEXT="$(cat issues_context.txt || true)" VERSIONRC="$(cat .versionrc.json || echo '{}')" USER_CONTENT=$(cat << EOF Repository: ${REPO} Release: ${CURR} Previous: ${PREV:-} Release URL: ${HTML_URL} Compare URL: ${COMPARE_URL} .versionrc.json configuration (sections to show/hide): --- ${VERSIONRC} --- Existing release notes (from conventional commits): --- ${EXISTING} --- Conventional commits (subjects): --- ${SUBJECTS} --- Commits (details): --- ${DETAILS} --- Changed files (for context on segment changes only, do not list paths in output): --- ${FILES} --- Referenced issues (for additional context, explain impact in user terms): --- ${ISSUES_CONTEXT} --- Contributors: --- ${CONTRIBUTORS} --- EOF ) echo " Calling GitHub Models API..." OUTPUT_MD="" set +e RESP=$(curl -sS -f -X POST "https://models.github.ai/inference/chat/completions" \ -H "Authorization: Bearer ${GH_TOKEN}" \ -H "Content-Type: application/json" \ -d "$(jq -n --arg model "$MODEL" --arg sys "$SYSTEM_PROMPT" --arg user "$USER_CONTENT" '{model:$model, messages: [{role:"system",content:$sys},{role:"user",content:$user}], temperature: 0.2, max_tokens: 4000}')") CURL_EXIT=$? if [ $CURL_EXIT -eq 0 ]; then OUTPUT_MD=$(printf "%s" "$RESP" | jq -r '.choices[0].message.content // empty') echo " ✅ API call successful" else echo " ❌ API call failed with exit code: $CURL_EXIT" echo " Response: $RESP" fi set -e if [[ -z "$OUTPUT_MD" ]]; then echo "❌ AI generation failed or no output produced." echo "enhanced_body=" >> "$GITHUB_OUTPUT" exit 0 fi echo " Generated changelog length: $(printf "%s" "$OUTPUT_MD" | wc -c) characters" # Save the AI-generated changelog echo "$OUTPUT_MD" > enhanced_changelog.md echo "✅ Enhanced changelog saved to enhanced_changelog.md" echo "enhanced_body<> "$GITHUB_OUTPUT" cat enhanced_changelog.md >> "$GITHUB_OUTPUT" echo "EOF" >> "$GITHUB_OUTPUT" echo "✅ Changelog saved to step output" - name: Update release body if: ${{ steps.ai.outputs.enhanced_body != '' }} shell: bash env: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail set -a; source ctx.env; set +a if [[ "$DRY_RUN" == "true" ]]; then echo "🧪 Dry run mode enabled - skipping release update" echo " The generated changelog would be applied to release ID: ${RELEASE_ID}" exit 0 fi echo "📝 Updating release body for release ID: ${RELEASE_ID}" # Use the AI-generated changelog as the complete release body PAYLOAD=$(jq -Rs '{body: .}' < enhanced_changelog.md) gh api -X PATCH repos/${{ github.repository }}/releases/${RELEASE_ID} -H "Content-Type: application/json" -d "$PAYLOAD" echo "✅ Release body updated successfully" - name: Summary if: ${{ always() && (inputs.dry_run || steps.ai.outputs.enhanced_body != '') }} shell: bash run: | set -a; source ctx.env; set +a echo "📊 Generating summary..." if [[ "$DRY_RUN" == "true" ]]; then echo "## 🧪 Dry Run - Enhanced Changelog Preview" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY if [[ -f enhanced_changelog.md ]]; then echo "**Release would not be modified.** Below is the generated changelog:" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY cat enhanced_changelog.md >> $GITHUB_STEP_SUMMARY else echo "⚠️ **AI generation failed or no changelog was produced.**" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "Check the workflow logs for details." >> $GITHUB_STEP_SUMMARY fi else echo "## ✅ Enhanced Changelog Generated" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "Release body has been updated with AI-enhanced changelog:" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY cat enhanced_changelog.md >> $GITHUB_STEP_SUMMARY fi echo "✅ Summary generated" - name: Skipped notice if: ${{ steps.ai.outputs.enhanced_body == '' }} run: | echo "❌ AI changelog generation skipped or failed. Ensure GitHub Models access is enabled for this repo." >> $GITHUB_STEP_SUMMARY ================================================ FILE: .github/workflows/android.yml ================================================ name: Android on: release: types: [published] jobs: build-android: runs-on: ubuntu-latest container: ghcr.io/jandedobbeleer/golang-android-container:latest steps: - name: Checkout code 👋 uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 - name: Build run: | VERSION=$(echo "${{ github.event.release.name }}" | cut -c2-) echo "Building version ${VERSION}" cd src go build -o dist/posh-android-arm -ldflags="-s -w -X 'github.com/jandedobbeleer/oh-my-posh/src/build.Version=${VERSION}' -X 'github.com/jandedobbeleer/oh-my-posh/src/build.Date=$(date)'" - name: Upload artifacts 🆙 uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd with: github-token: ${{secrets.GITHUB_TOKEN}} script: | console.log('environment', process.versions); const fs = require('fs').promises; const { repo: { owner, repo }, sha } = context; console.log({ owner, repo, sha }); await github.rest.repos.uploadReleaseAsset({ owner, repo, release_id: ${{ github.event.release.id }}, name: 'posh-android-arm', data: await fs.readFile('./src/dist/posh-android-arm') }); ================================================ FILE: .github/workflows/bluesky.yml ================================================ name: Bluesky on: release: types: [published] workflow_dispatch: jobs: bluesky: runs-on: ubuntu-latest steps: - name: Publish uses: JanDeDobbeleer/bluesky-releasenotes-action@main with: title: "The best release yet 🚀" bluesky-identifier: ${{ secrets.BLUESKY_IDENTIFIER }} bluesky-password: ${{ secrets.BLUESKY_PASSWORD }} github-token: ${{ secrets.GH_PAT }} ================================================ FILE: .github/workflows/build_code.yml ================================================ on: pull_request: paths-ignore: - 'README.md' - 'CONTRIBUTING.md' - 'COPYING' - 'website/**' - '.github/*.md' - '.github/FUNDING.yml' concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true name: Build Code jobs: build: runs-on: macos-latest defaults: run: shell: pwsh steps: - name: Checkout code 👋 uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 - name: Install Go 🗳 uses: ./.github/workflows/composite/bootstrap-go - name: Run GoReleaser 🚀 uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 with: distribution: goreleaser version: v2.3.2 args: build --clean --snapshot --skip=post-hooks --skip=before workdir: src - name: Archive production artifacts uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f with: name: builds retention-days: 1 path: | src/dist ================================================ FILE: .github/workflows/close_themes_pr.yml ================================================ name: Close Themes PR on: pull_request_target: types: - opened jobs: check: runs-on: ubuntu-latest steps: - name: Checkout code 👋 uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 - name: Check and close 🔐 uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd with: github-token: ${{ secrets.GH_PAT }} script: | const { repo: { owner, repo } } = context; const pr = context.payload.pull_request; const response = await github.rest.pulls.listFiles({ owner, repo, pull_number: pr.number }); if (response.status !== 200) { console.log('Could not fetch files'); return; } let hasThemeAdditions = false; for (const file of response.data) { const name = file.filename console.log(`File: ${name}`); if (file.status === 'added' && name.includes('themes/')) { console.log(`File: ${name} is a theme addition`); hasThemeAdditions = true; break; } } if (!hasThemeAdditions) { console.log('No theme additions found.'); return; } const body = `👋 @${pr.user.login}, theme aditions are no longer accepted due to the ever growing set. We do however accept showcasing your custom theme in the [🎨 Themes section](https://github.com/JanDeDobbeleer/oh-my-posh/discussions/categories/themes) or [themes channel](https://discord.com/channels/1023597603331526656/1055533233309233252) on Discord.` console.log(`Adding comment: ${body}`); await github.rest.issues.createComment({ owner, repo, issue_number: pr.number, body, }); console.log(`Closing pull request: ${pr.html_url}`); await github.rest.pulls.update({ owner, repo, pull_number: pr.number, state: "closed", }); ================================================ FILE: .github/workflows/code.yml ================================================ on: pull_request: paths-ignore: - 'README.md' - 'CONTRIBUTING.md' - 'COPYING' - 'website/**' - '.github/*.md' - '.github/FUNDING.yml' concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true name: Validate Code jobs: test: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} defaults: run: working-directory: ${{ github.workspace }}/src steps: - name: Checkout code uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 - name: Install Go 🗳 uses: ./.github/workflows/composite/bootstrap-go - name: Golang CI uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 with: working-directory: src - name: Fieldalignment run: | go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latest fieldalignment "./..." - name: Modernize run: | go install golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@latest modernize "./..." - name: Unit Tests run: go test "./..." ================================================ FILE: .github/workflows/commits.yml ================================================ name: Validate Commits on: [pull_request] concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: commitlint: uses: jandedobbeleer/workflows/.github/workflows/commits.yml@main ================================================ FILE: .github/workflows/composite/bootstrap-go/action.yml ================================================ # yaml-language-server: $schema=https://json.schemastore.org/github-action.json name: "Setup Go" description: "Install Go and override with the custom build" branding: icon: download color: purple runs: using: "composite" steps: - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 with: go-version: "1.26.0" cache-dependency-path: src/go.sum ================================================ FILE: .github/workflows/contributors.yml ================================================ name: Contributors on: pull_request_target: types: - closed jobs: contributors: uses: jandedobbeleer/workflows/.github/workflows/contributors.yml@main secrets: token: ${{ secrets.GH_PAT }} ================================================ FILE: .github/workflows/copilot-setup-steps.yml ================================================ name: "Copilot Setup Steps" on: workflow_dispatch: push: paths: - .github/workflows/copilot-setup-steps.yml pull_request: paths: - .github/workflows/copilot-setup-steps.yml jobs: copilot-setup-steps: runs-on: ubuntu-latest permissions: contents: read steps: - name: Checkout code uses: actions/checkout@v6 - name: Install apm-cli and run apm install run: | pip install apm-cli apm install ================================================ FILE: .github/workflows/delete_store_submission.yml ================================================ name: Delete Store Submission on: workflow_dispatch: jobs: delete_submission: name: Delete Store Submission runs-on: ubuntu-latest steps: - name: Configure Store Credentials 🔑 uses: jandedobbeleer/store-submission@submission-status with: command: configure type: win32 seller-id: ${{ secrets.SELLER_ID }} product-id: ${{ secrets.PRODUCT_ID }} tenant-id: ${{ secrets.TENANT_ID }} client-id: ${{ secrets.CLIENT_ID }} client-secret: ${{ secrets.CLIENT_SECRET }} - name: Delete Submission 🗑️ uses: jandedobbeleer/store-submission@submission-status with: command: delete ================================================ FILE: .github/workflows/dependabot.yml ================================================ name: Dependabot auto-merge on: pull_request: types: [opened, reopened] permissions: contents: write pull-requests: write jobs: dependabot: uses: jandedobbeleer/workflows/.github/workflows/dependabot.yml@main ================================================ FILE: .github/workflows/discord.yml ================================================ name: Discord on: release: types: [published] jobs: notify: uses: jandedobbeleer/workflows/.github/workflows/discord.yml@main secrets: webhook: ${{ secrets.CHANGELOG_WEBHOOK }} ================================================ FILE: .github/workflows/docs.yml ================================================ name: Azure Static Web Apps CI/CD on: push: branches: - main paths: - "website/**" - "themes/**" workflow_dispatch: permissions: id-token: write contents: read jobs: build_and_deploy: runs-on: ubuntu-latest name: Build and Deploy steps: - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 with: submodules: true persist-credentials: false - name: Install Go 🗳 uses: ./.github/workflows/composite/bootstrap-go - name: Setup Node.js uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f with: node-version: 20.9.0 # Create Kind cluster to have a Kubernetes context for cloud-native-azure theme # Images are defined on every Kind release # See https://github.com/kubernetes-sigs/kind/releases - name: Create k8s v1.23 Kind Cluster uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc with: node_image: kindest/node:v1.23.4@sha256:0e34f0d0fd448aa2f2819cfd74e99fe5793a6e4938b328f657c8e3f81ee0dfb9 cluster_name: posh - name: Create Kubernetes namespace run: | kubectl create ns demo - name: Set default Kubernetes namespace run: | kubectl config set-context posh --namespace demo - uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 with: creds: ${{ secrets.AZURE_CREDENTIALS }} - name: Build oh-my-posh 🔧 run: | cd src go build -o ./bin/oh-my-posh cd .. - name: Render themes 🎨 run: | export PATH="$PWD/src/bin:$PATH" cd website npm install npm run themes cd .. - name: Copy schema for MCP validator 📋 run: | mkdir -p website/api/data cp themes/schema.json website/api/data/schema.json echo "✅ Copied schema.json to website/api/data/" - name: Build Docs And Deploy 🚀 id: builddeploy uses: Azure/static-web-apps-deploy@1a947af9992250f3bc2e68ad0754c0b0c11566c9 with: azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_ASHY_MEADOW_063E9BA03 }} repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for GitHub integrations (i.e. PR comments) action: "upload" ###### Repository/Build Configurations - These values can be configured to match your app requirements. ###### # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig app_location: "/website" # App source code path api_location: "/website/api" # Api source code path - optional output_location: "build" # Built app content directory - optional ================================================ FILE: .github/workflows/edit_rights.yml ================================================ name: Notify When Maintainers Cannot Edit # **What it does**: Notifies the author of a PR when their PR does not allow maintainers to edit it. # **Why we have it**: To prevent having to do this manually. # **Who does it impact**: Open-source. on: pull_request_target: types: - opened - edited permissions: pull-requests: write jobs: notify-when-maintainers-cannot-edit: uses: jandedobbeleer/workflows/.github/workflows/edit_rights.yml@main secrets: token: ${{ secrets.GH_PAT }} ================================================ FILE: .github/workflows/gomod.yml ================================================ name: Go Mod on: [pull_request] concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: go-mod: runs-on: ubuntu-latest defaults: run: working-directory: ${{ github.workspace }}/src steps: - name: Checkout code uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 - name: Install Go 🗳 uses: ./.github/workflows/composite/bootstrap-go - name: Check for unused dependencies run: | go mod tidy if [ "$(git status | grep -c "nothing to commit, working tree clean")" -eq 1 ]; then echo "Nothing to tidy" exit 0 fi echo "Go mod tidy is needed" exit 1 ================================================ FILE: .github/workflows/homebrew.yml ================================================ name: Homebrew on: release: types: [published] jobs: notify: runs-on: ubuntu-latest steps: - name: Notify Homebrew Repo 🙋🏾‍♀️ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd with: github-token: ${{ secrets.GH_PAT }} script: | await github.request('POST /repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches', { owner: 'jandedobbeleer', repo: 'homebrew-oh-my-posh', workflow_id: 'release.yml', ref: 'main', inputs: {"version": process.env.GITHUB_REF.replace('refs/tags/v', '')} }) ================================================ FILE: .github/workflows/lock.yml ================================================ name: 'Lock Threads' on: schedule: - cron: '0 0 * * 1' permissions: issues: write concurrency: group: lock jobs: action: runs-on: ubuntu-latest steps: - uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 with: issue-inactive-days: '90' issue-comment: > This issue has been automatically locked since there has not been any recent activity (i.e. last half year) after it was closed. It helps our maintainers focus on the active issues. If you have found a problem that seems similar, please open a [discussion](https://github.com/JanDeDobbeleer/oh-my-posh/discussions/new?category=troubleshoot) first, complete the body with all the details necessary to reproduce, and mention this issue as reference. process-only: 'issues' ================================================ FILE: .github/workflows/markdown.yml ================================================ name: Markdownlint on: [pull_request] concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: lint: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 - name: Lint files uses: DavidAnson/markdownlint-cli2-action@07035fd053f7be764496c0f8d8f9f41f98305101 with: config: .markdownlint-cli2.yaml globs: '**/*.md' ================================================ FILE: .github/workflows/merge_contributions_pr.yml ================================================ name: Merge contributions PR on: pull_request_target: types: - opened - reopened jobs: check: runs-on: ubuntu-latest steps: - name: Checkout code 👋 uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 - name: Check and merge ⛙ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd with: github-token: ${{ secrets.GH_PAT }} script: | const { repo: { owner, repo } } = context; const pr = context.payload.pull_request; if (pr.user.id !== 46447321) { console.log('Not an all-contributors pull request'); return; } console.log(`Merging pull request: ${pr.html_url}`); await github.rest.pulls.merge({ owner, repo, pull_number: pr.number, merge_method: "rebase", }); ================================================ FILE: .github/workflows/microsoft_store.yml ================================================ name: Windows Store on: release: types: [published] jobs: microsoft_store: name: Publish To Windows Store runs-on: ubuntu-latest steps: - name: Configure Store Credentials 🔑 uses: jandedobbeleer/store-submission@submission-status with: command: configure type: win32 seller-id: ${{ secrets.SELLER_ID }} product-id: ${{ secrets.PRODUCT_ID }} tenant-id: ${{ secrets.TENANT_ID }} client-id: ${{ secrets.CLIENT_ID }} client-secret: ${{ secrets.CLIENT_SECRET }} only-on-ready: true - name: Update draft submission uses: jandedobbeleer/store-submission@submission-status with: command: update product-update: '{ "packages":[ { "packageUrl":"https://github.com/JanDeDobbeleer/oh-my-posh/releases/download/${{ github.event.release.tag_name }}/install-x64.msi", "languages":["en"], "architectures":["X64"], "installerParameters":"/quiet INSTALLER=ws", "isSilentInstall":false }, { "packageUrl":"https://github.com/JanDeDobbeleer/oh-my-posh/releases/download/${{ github.event.release.tag_name }}/install-arm64.msi", "languages":["en"], "architectures":["Arm64"], "installerParameters":"/quiet INSTALLER=ws", "isSilentInstall":false } ] }' - name: Publish Submission uses: jandedobbeleer/store-submission@submission-status with: command: publish ================================================ FILE: .github/workflows/publish-mcp.yml ================================================ name: Publish to MCP Registry on: push: branches: - main paths: - 'website/api/mcp/**' workflow_dispatch: jobs: publish: name: Publish MCP Server runs-on: ubuntu-latest permissions: id-token: write # Required for OIDC authentication contents: read steps: - name: Checkout code uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 with: fetch-depth: 0 # Fetch all tags - name: Extract version from latest git tag id: version run: | # Get the latest git tag (without 'v' prefix) VERSION=$(git describe --tags --abbrev=0 | sed 's/^v//') echo "version=$VERSION" >> $GITHUB_OUTPUT echo "Using version from git tag: $VERSION" - name: Update server.json version run: | cd website/api/mcp jq --arg v "${{ steps.version.outputs.version }}" '.version = $v' server.json > tmp.json && mv tmp.json server.json echo "Updated server.json version to ${{ steps.version.outputs.version }}" cat server.json - name: Validate server.json run: | cd website/api npm ci cd mcp node validate-server.js - name: Install MCP Publisher run: | curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar -xzf - chmod +x mcp-publisher ./mcp-publisher --version - name: Login to MCP Registry env: MCP_REGISTRY_PEM: ${{ secrets.MCP_REGISTRY_PEM }} run: | echo "$MCP_REGISTRY_PEM" > key.pem PRIVATE_KEY=$(openssl pkey -in key.pem -noout -text | grep -A3 "priv:" | tail -n +2 | tr -d ' :\n') ./mcp-publisher login dns --domain ohmyposh.dev --private-key "$PRIVATE_KEY" rm -f key.pem - name: Publish to MCP Registry run: | cd website/api/mcp ../../../mcp-publisher publish ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: push: branches: - main paths: - "src/**" - "packages/**" - ".github/workflows/**" workflow_dispatch: concurrency: group: ${{ github.workflow }} jobs: changelog: runs-on: ubuntu-latest outputs: version: ${{ steps.changelog.outputs.version }} body: ${{ steps.changelog.outputs.clean_changelog }} tag: ${{ steps.changelog.outputs.tag }} skipped: ${{ steps.changelog.outputs.skipped }} steps: - name: Checkout code 👋 uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 - name: Create changelog ✍️ id: changelog uses: TriPSs/conventional-changelog-action@91be4f3188da74fe85de9caffcebc80b26d43b5b with: github-token: ${{ secrets.github_token }} skip-version-file: "true" output-file: "false" skip-commit: "true" skip-on-empty: "true" skip-tag: "true" artifacts: needs: changelog if: ${{ needs.changelog.outputs.skipped == 'false' }} runs-on: windows-latest defaults: run: shell: pwsh working-directory: ${{ github.workspace }}/build steps: - name: Checkout code 👋 uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 - name: Install Go 🗳 uses: ./.github/workflows/composite/bootstrap-go - name: Pre Build 😸 env: SIGNING_KEY: ${{ secrets.SIGNING_KEY }} run: | ./pre.ps1 -Version ${{ needs.changelog.outputs.version }} -SDKVersion "10.0.26100.0" - name: Run GoReleaser 🚀 uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 with: distribution: goreleaser version: v2.3.2 args: release --clean --skip publish workdir: src env: AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} - name: Post Build 🤐 run: | ./post.ps1 - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f with: name: build-artifacts path: | src/dist/posh-* src/dist/themes.* src/dist/checksums.* msi: needs: - changelog - artifacts runs-on: windows-latest strategy: matrix: arch: [x64, arm64] defaults: run: shell: pwsh working-directory: ${{ github.workspace }}/packages/msi steps: - name: Checkout code 👋 uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c with: name: build-artifacts path: dist - name: Install Wix Toolset 🛠 run: dotnet tool install --global wix - name: Build installer 📦 id: build env: AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} run: | $version = '${{ needs.changelog.outputs.version }}'.TrimStart("v") ./build.ps1 -Architecture ${{ matrix.arch }} -Version $version -Copy -Sign -SDKVersion "10.0.26100.0" - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f with: name: msi-artifact-${{ matrix.arch }} path: | packages/msi/out/install-${{ matrix.arch }}.msi packages/msi/out/install-${{ matrix.arch }}.msix release: runs-on: ubuntu-latest needs: - changelog - artifacts - msi steps: - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c with: merge-multiple: true - name: Upload version file env: AZURE_STORAGE_CONNECTION_STRING: ${{ secrets.CDN_CONNECTIONSTRING }} run: | echo v${{ needs.changelog.outputs.version }} > version.txt az storage blob upload-batch --destination releases/v${{ needs.changelog.outputs.version }} --source . az storage blob upload-batch --destination releases/latest --overwrite true --source . - name: Release 🎓 uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe with: tag_name: ${{ needs.changelog.outputs.tag }} body: ${{ needs.changelog.outputs.body }} fail_on_unmatched_files: true token: ${{ secrets.GH_PAT }} files: | * winget: runs-on: windows-latest needs: - changelog - release env: WINGETCREATE_TOKEN: ${{ secrets.WINGETCREATE_TOKEN }} steps: - name: Create manifest and submit PR 📦 shell: pwsh run: | Write-Host "Preparing to submit to WinGet repository..." -ForegroundColor Green # Install the latest wingetcreate exe # Need to do things this way, see https://github.com/PowerShell/PowerShell/issues/13138 Write-Verbose "Importing Appx module using Windows PowerShell compatibility" Import-Module Appx -UseWindowsPowerShell -ErrorAction Stop # Download and install Winget-Create msixbundle $appxBundleFile = Join-Path -Path $env:TEMP -ChildPath "wingetcreate.msixbundle" Write-Verbose "Downloading wingetcreate to: $appxBundleFile" Invoke-WebRequest -Uri "https://aka.ms/wingetcreate/latest/msixbundle" -OutFile $appxBundleFile -ErrorAction Stop Add-AppxPackage -Path $appxBundleFile -ErrorAction Stop Write-Verbose "Successfully installed wingetcreate" # Submit the PR to WinGet repository Write-Host "Submitting pull request to WinGet repository..." -ForegroundColor Green $version = "${{ needs.changelog.outputs.tag }}" $version = $version.TrimStart('v') $urls = @( "https://github.com/JanDeDobbeleer/oh-my-posh/releases/download/v$version/install-x64.msi|x64", "https://github.com/JanDeDobbeleer/oh-my-posh/releases/download/v$version/install-x64.msix|x64", "https://github.com/JanDeDobbeleer/oh-my-posh/releases/download/v$version/install-arm64.msi|arm64", "https://github.com/JanDeDobbeleer/oh-my-posh/releases/download/v$version/install-arm64.msix|arm64" ) wingetcreate update JanDeDobbeleer.OhMyPosh --version $version --token $env:WINGETCREATE_TOKEN --submit --urls $urls ================================================ FILE: .github/workflows/vale.yml ================================================ name: Vale on: [pull_request] concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: vale: name: runner / vale runs-on: ubuntu-latest steps: - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v4 - name: Install Vale run: | curl -sfL https://github.com/errata-ai/vale/releases/download/v3.13.1/vale_3.13.1_Linux_64-bit.tar.gz | tar -xz sudo mv vale /usr/local/bin/vale - name: Sync Vale packages run: vale sync - name: Lint run: vale AGENTS.md .github/copilot-instructions.md .github/skills ================================================ FILE: .gitignore ================================================ # APM apm_modules/ .github/skills/* !.github/skills/segment-create/ !.github/skills/segment-docs/ # Others .specs/ .fleet/ src/test/umbraco/obj/ src/keys *.prof *.wixpdb packages/msi/Microsoft.Trusted.Signing.Client .claude .styles # Created by https://www.toptal.com/developers/gitignore/api/node,go,visualstudiocode # Edit at https://www.toptal.com/developers/gitignore?templates=node,go,visualstudiocode ### Go ### # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Windows asset files /src/rsrc_windows_*.syso # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Dependency directories (remove the comment below to include it) # vendor/ ### Go Patch ### /vendor/ /Godeps/ ### Node ### # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage *.lcov # nyc test coverage .nyc_output # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # TypeScript v1 declaration files typings/ # TypeScript cache *.tsbuildinfo # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Microbundle cache .rpt2_cache/ .rts2_cache_cjs/ .rts2_cache_es/ .rts2_cache_umd/ # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variables file .env .env.test # parcel-bundler cache (https://parceljs.org/) .cache # Next.js build output .next # Nuxt.js build / generate output .nuxt dist # Gatsby files .cache/ # Comment in the public line in if your project uses Gatsby and not Next.js # https://nextjs.org/blog/next-9-1#public-directory-support # public # vuepress build output .vuepress/dist # Serverless directories .serverless/ # FuseBox cache .fusebox/ # DynamoDB Local files .dynamodb/ # TernJS port file .tern-port # Stores VSCode versions used for testing VSCode extensions .vscode-test ### VisualStudioCode ### .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json *.code-workspace ### VisualStudioCode Patch ### # Ignore all local history of files .history # End of https://www.toptal.com/developers/gitignore/api/node,go,visualstudiocode # linux binary /src/oh-my-posh package/ bin/ Output/ *.sha256 *.7z # images *.png # go releaser /src/dist # Created by https://www.toptal.com/developers/gitignore/api/windows,linux,macos # Edit at https://www.toptal.com/developers/gitignore?templates=windows,linux,macos ### Linux ### *~ # temporary files which can be created if a process still has a handle open of a deleted file .fuse_hidden* # KDE directory preferences .directory # Linux trash folder which might appear on any partition or disk .Trash-* # .nfs files are created when an open file is removed but is still being accessed .nfs* ### macOS ### # General .DS_Store .AppleDouble .LSOverride # Icon must end with two \r Icon # Thumbnails ._* # Files that might appear in the root of a volume .DocumentRevisions-V100 .fseventsd .Spotlight-V100 .TemporaryItems .Trashes .VolumeIcon.icns .com.apple.timemachine.donotpresent # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk ### Windows ### # Windows thumbnail cache files Thumbs.db Thumbs.db:encryptable ehthumbs.db ehthumbs_vista.db # Dump file *.stackdump # Folder config file [Dd]esktop.ini # Recycle Bin used on file shares $RECYCLE.BIN/ # Windows Installer files *.cab *.msi *.msix *.msm *.msp # Windows shortcuts *.lnk # End of https://www.toptal.com/developers/gitignore/api/windows,linux,macos # Keys cosign.key *.omp.json.bak __debug_bin src/src ================================================ FILE: .markdownlint-cli2.yaml ================================================ config: MD013: line_length: 120 code_blocks: false MD024: false fix: true gitignore: true ignores: - node_modules/ - .github/agents/segment-docs.md - .github/agents/architecture.md - .github/PULL_REQUEST_TEMPLATE.md ================================================ FILE: .prettierrc ================================================ { "trailingComma": "none", "overrides": [ { "files": ["*.jsonc", "*.json"], "options": { "parser": "json", "trailingComma": "none" } } ] } ================================================ FILE: .vale.ini ================================================ StylesPath = .styles MinAlertLevel = suggestion Packages = https://github.com/tbhb/vale-ai-tells/releases/download/v1.4.0/ai-tells.zip, https://github.com/HeyItsGilbert/vale-agentic/releases/download/v2.0.0/agentic.zip [*.{md}] # ^ This section applies to only Markdown files. # # You can change (or add) file extensions here # to apply these settings to other file types. # # For example, to apply these settings to both # Markdown and reStructuredText: # # [*.{md,rst}] BasedOnStyles = ai-tells, agentic ================================================ FILE: .versionrc.json ================================================ { "types": [ { "type": "feat", "section": "Features" }, { "type": "fix", "section": "Bug Fixes" }, { "type": "refactor", "section": "Refactor" }, { "type": "revert", "section": "Reverts" }, { "type": "theme", "section": "Themes" }, { "type": "chore", "hidden": true }, { "type": "ci", "hidden": true }, { "type": "chore", "hidden": true }, { "type": "docs", "hidden": true }, { "type": "perf", "hidden": true }, { "type": "test", "hidden": true } ] } ================================================ FILE: .vscode/extensions.json ================================================ { "recommendations": [ "bmalehorn.vscode-fish", "davidanson.vscode-markdownlint", "elves.elvish", "esbenp.prettier-vscode", "github.vscode-pull-request-github", "golang.go", "jnoortheen.xonsh", "ms-azuretools.vscode-azurefunctions", "ms-vscode.powershell", "redhat.vscode-yaml", "sumneko.lua", "tamasfe.even-better-toml", "yzhang.markdown-all-in-one" ] } ================================================ FILE: .vscode/launch.json ================================================ { "version": "0.2.0", "configurations": [ { "name": "Primary", "type": "go", "request": "launch", "mode": "debug", "program": "${workspaceRoot}/src", "args": [ "print", "primary", "--shell=pwsh", "--terminal-width=200" ] }, { "name": "Tooltip", "type": "go", "request": "launch", "mode": "debug", "program": "${workspaceRoot}/src", "args": [ "print", "tooltip", "--command=git", "--shell=pwsh" ] }, { "name": "Transient", "type": "go", "request": "launch", "mode": "debug", "program": "${workspaceRoot}/src", "args": [ "print", "transient", "--shell=pwsh", "--status=1" ] }, { "name": "Launch tests", "type": "go", "request": "launch", "mode": "test", "program": "${workspaceRoot}/src", "args": [ "--test.v" ] }, { "name": "Debug", "type": "go", "request": "launch", "mode": "debug", "program": "${workspaceRoot}/src", "args": [ "debug" ] }, { "name": "Init", "type": "go", "request": "launch", "mode": "debug", "program": "${workspaceRoot}/src", "args": [ "init", "cmd", "--print" ] }, { "name": "Export Config", "type": "go", "request": "launch", "mode": "debug", "program": "${workspaceRoot}/src", "args": [ "config", "export" ] }, { "name": "Export Image", "type": "go", "request": "launch", "mode": "debug", "program": "${workspaceRoot}/src", "args": [ "config", "export", "image" ] }, { "name": "Migrate config", "type": "go", "request": "launch", "mode": "debug", "program": "${workspaceRoot}/src", "args": [ "config", "migrate" ] }, { "name": "Migrate glyphs", "type": "go", "request": "launch", "mode": "debug", "program": "${workspaceRoot}/src", "args": [ "config", "migrate", "glyphs" ] }, { "name": "Get value", "type": "go", "request": "launch", "mode": "debug", "program": "${workspaceRoot}/src", "args": [ "get", "accent" ] }, { "name": "Toggle segment", "type": "go", "request": "launch", "mode": "debug", "program": "${workspaceRoot}/src", "args": [ "toggle", "git" ] }, { "name": "Notice", "type": "go", "request": "launch", "mode": "debug", "program": "${workspaceRoot}/src", "args": [ "notice" ] }, { "name": "Upgrade", "type": "go", "request": "launch", "mode": "debug", "program": "${workspaceRoot}/src", "args": [ "upgrade" ] }, { "name": "Font install", "type": "go", "request": "launch", "mode": "debug", "program": "${workspaceRoot}/src", "args": [ "font", "install", "AnonymousPro" ] }, { "name": "Auth YTMDA", "type": "go", "request": "launch", "mode": "debug", "program": "${workspaceRoot}/src", "args": [ "auth", "ytmda" ] }, { "name": "DSC schema", "type": "go", "request": "launch", "mode": "debug", "program": "${workspaceRoot}/src", "args": [ "font", "dsc", "schema" ] }, { "type": "node", "request": "launch", "name": "Theme export", "cwd": "${workspaceFolder}/website", "program": "${workspaceRoot}/website/export_themes.mjs", "console": "integratedTerminal" }, { "type": "node", "request": "launch", "name": "Bluesky", "cwd": "${workspaceFolder}/scripts/bluesky", "program": "${workspaceRoot}/scripts/bluesky/main.cjs", "console": "integratedTerminal", "envFile": "${workspaceFolder}/scripts/bluesky/.env" }, { "name": "Docs API", "type": "node", "request": "attach", "port": 9229, "preLaunchTask": "func: host start", "cwd": "${workspaceFolder}/website", "envFile": "${workspaceFolder}/website/.env" }, { "name": "Cache clear", "type": "go", "request": "launch", "mode": "debug", "program": "${workspaceRoot}/src", "args": [ "cache", "clear" ] } ] } ================================================ FILE: .vscode/settings.json ================================================ { "go.lintTool": "golangci-lint", "go.useLanguageServer": true, "go.testOnSave": true, "[go]": { "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.organizeImports": "explicit" } }, "go.formatTool": "gofmt", "go.formatFlags": [ "-s" ], "azureFunctions.deploySubpath": "docs/api", "azureFunctions.postDeployTask": "npm install (functions)", "azureFunctions.projectLanguage": "JavaScript", "azureFunctions.projectRuntime": "~4", "debug.internalConsoleOptions": "neverOpen", "azureFunctions.projectSubpath": "docs/api", "azureFunctions.preDeployTask": "npm prune (functions)", "[markdown]": { "editor.formatOnSave": true, "editor.formatOnPaste": true, "editor.codeActionsOnSave": { "source.fixAll.markdownlint": "explicit" } }, "files.encoding": "utf8", "[powershell]": { "files.encoding": "utf8" } } ================================================ FILE: .vscode/tasks.json ================================================ { // See https://go.microsoft.com/fwlink/?LinkId=733558 // for the documentation about the tasks.json format "version": "2.0.0", "cwd": "${workspaceRoot}", "echoCommand": true, "type": "shell", "tasks": [ { "type": "shell", "command": "go", "label": "build oh-my-posh", "detail": "Build oh-my-posh in the /src folder locally", "options": { "cwd": "${workspaceRoot}/src" }, "group": { "kind": "build", "isDefault": true }, "problemMatcher": "$go", "args": [ "build", "-v" ] }, { "type": "shell", "command": "go", "label": "devcontainer: rebuild oh-my-posh", "detail": "Build oh-my-posh for all shells when inside the devcontainer", "options": { "cwd": "${workspaceRoot}/src", "shell": { "executable": "bash", "args": [ "-c" ] }, "statusbar": { "hide": false, "color": "#22C1D6", "label": "$(beaker) devcontainer: rebuild oh-my-posh", "tooltip": "Compiles *oh-my-posh* from this repo while **overwriting** your preinstalled stable release." } }, "group": "build", "problemMatcher": "$go", "args": [ "build", "-v", "-o", "/home/vscode/bin/oh-my-posh", "-ldflags", "\"-s -w -X 'github.com/jandedobbeleer/oh-my-posh/src/build.Version=development-$(git --no-pager log -1 --pretty=%h-%s)' -extldflags '-static'\"" ] }, { "type": "npm", "script": "start", "path": "website/", "problemMatcher": [], "label": "website: start", "detail": "cross-env NODE_ENV=development docusaurus start" }, { "type": "func", "command": "host start", "problemMatcher": "$func-node-watch", "isBackground": true, "dependsOn": "npm install (functions)", "options": { "cwd": "${workspaceFolder}/website/api" } }, { "type": "shell", "label": "npm install (functions)", "command": "npm install", "options": { "cwd": "${workspaceFolder}/website/api" } }, { "type": "shell", "label": "npm prune (functions)", "command": "npm prune --production", "problemMatcher": [], "options": { "cwd": "${workspaceFolder}/website/api" } } ] } ================================================ FILE: AGENTS.md ================================================ # Agent Instructions ## APM Setup This repository uses [APM](https://github.com/JanDeDobbeleer/agentic) to manage agent skills. Before starting any task, verify that the skills listed in `apm.yml` are installed under the `apm_modules/` directory. If `apm_modules/` is missing or any skill package from `apm.yml` is not present, install them by running: ```sh pip install apm-cli apm install ``` ## General File Creation Guidelines When creating new files: - **Always use LF (Unix-style) line endings**, not CRLF (Windows-style) - This repository uses `.gitattributes` to enforce LF line endings - Ensures consistency across all platforms and avoids Git warnings ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement by reaching out via [email](mailto:abuse@ohmyposh.dev). All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available [in the documentation][version-2]. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][moz-div]. For answers to common questions about this code of conduct, see the [FAQ][faq]. Translations are available [in the documentation][translations]. [homepage]: https://www.contributor-covenant.org [moz-div]: https://github.com/mozilla/diversity [version-2]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html [faq]: https://www.contributor-covenant.org/faq [translations]: https://www.contributor-covenant.org/translations ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing Note we have a code of conduct, please follow it in all your interactions with the project. > [!NOTE] > Theme additions are no longer accepted due to the ever growing set. > We do however accept showcasing your custom theme in the [themes discussion section here][themes-discussion] > or the [themes channel on Discord][discord-link]. Ensure you've read through the [documentation][docs] so you understand the core concepts of the project. If you're looking to get familiar with go, following the getting started [guide][guide] can be a good starting point. ## Setting Up Agents and Skills This project uses [APM (Agent Package Manager)][apm] to manage shared AI agent skills. Project-specific skills live in `.github/skills/`, while shared skills are declared in `apm.yml` and installed via APM. ### Install APM ```bash curl -sSL https://raw.githubusercontent.com/microsoft/apm/main/install.sh | sh ``` Alternatively, install via Homebrew or pip: ```bash brew install microsoft/apm/apm # or pip install apm-cli ``` ### Install Skills After cloning the repository, run: ```bash apm install ``` This pulls in the shared skills from [JanDeDobbeleer/agentic][agentic] (conventional commits, Go, Markdown, and PowerShell conventions). The project-specific skills (segment-create and segment-docs) are already included in the repository. ## Pull Request Process 1. Ensure any dependencies or build artifacts are removed/ignored before creating a commit. 2. Commits follow the [conventional commits][cc] guidelines. (You can [look up the supported *types*][cc-types] along with an explanation [in the documentation][cc-types]) 3. Update the documentation with details of changes to the functionality, this includes new segments or core functionality. 4. Pull Requests are merged once all checks pass and a project maintainer has approved it. ## Codespaces / Devcontainer Development Environment Arguably the easiest way to contribute anything is to use our prepared development environment. We have a `.devcontainer/devcontainer.json` file, meaning we are compatible with: - [![Open in GitHub Codespaces][codespaces-badge]][codespaces-link], or - the [Visual Studio Code Remote - Containers][devcontainer-ext] extension. This Linux environment includes all shells supported by oh-my-posh, including Bash, ZSH, Fish and PowerShell, the latter of which is the default. ### Configuring Devcontainer's Timezone & Theme 1. Open the [`.devcontainer/devcontainer.json`][devcontainer] file and in the "*build*" section modify: - `TZ`: with [your own timezone][timezones] 2. Summon the Command Panel (Ctrl+Shift+P) and select `Codespaces: Rebuild Container` to rebuild your devcontainer. (This should take just a few seconds.) ### Recompiling oh-my-posh The devcontainer definition preinstalls the latest stable oh-my-posh release at build time. To overwrite the installation's version inside the running devcontainer, you may use the VSCode *task* `devcontainer: build omp` to rebuild your oh-my-posh with that of your running repository's state. (You might see a button for this in your statusbar.) If the compile succeeds, `oh-my-posh --version` should reply: `development` Should you somehow mess up your devcontainer's OMP install catastrophically, remember that if you do `Codespaces: Rebuild Container` again, you'll be back to the latest stable release. ## Local development Make sure your local go version matches with the pinned version in [go.mod]. You can build oh-my-posh by navigating the to the `/src` folder and executing the following command. ```bash go build -v -o /path/to/oh-my-posh(.exe) ``` ### Running tests To execute the tests, run the following command from the `/src` folder. ```bash go test "./..." ``` [themes-discussion]: [https://github.com/JanDeDobbeleer/oh-my-posh/discussions/categories/themes] [discord-link]: [https://discord.com/channels/1023597603331526656/1055533233309233252] [docs]: [guide]: [cc]: [cc-types]: [codespaces-badge]: [codespaces-link]: [devcontainer-ext]: [timezones]: [devcontainer]: .devcontainer/devcontainer.json [go.mod]: src/go.mod [apm]: https://github.com/microsoft/apm [agentic]: https://github.com/JanDeDobbeleer/agentic ================================================ FILE: COPYING ================================================ Copyright 2022 Jan De Dobbeleer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================

Oh My Posh logo – Prompt theme engine for any shell

![MIT license badge](https://img.shields.io/github/license/JanDeDobbeleer/oh-my-posh.svg) ![Build Status badge](https://img.shields.io/github/actions/workflow/status/jandedobbeleer/oh-my-posh/release.yml?branch=main) [![Release version number badge][release-badge]][release] [![Documentation link badge ohmyposh.dev][docs-badge]][docs] ![Number of GitHub Downloads badge](https://img.shields.io/github/downloads/jandedobbeleer/oh-my-posh/total?color=pink&label=GitHub%20Downloads) This repo was made with love using GitKraken. [![GitKraken shield][kraken]][kraken-ref] ## Sponsors [![Documentation link badge ohmyposh.dev][merge-conflict-logo]][merge-conflict] [Want to become a sponsor?][sponsor-link] ## Join the community ![Mastodon badge](https://img.shields.io/mastodon/follow/110275292073181892?domain=https%3A%2F%2Fhachyderm.io&label=Mastodon&style=social) ![Discord badge](https://img.shields.io/discord/1023597603331526656) What started as the offspring of [oh-my-posh2](https://github.com/JanDeDobbeleer/oh-my-posh2) for PowerShell resulted in a cross platform, highly customizable and extensible prompt theme engine. After 4 years of working on oh-my-posh, a modern and more efficient tool was needed to suit my personal needs. ## :heart: Support :heart: [![Swag][swag-badge]][swag] - Show your love with a t-shirt! [![GitHub][github-badge]][github-sponsors] - One time support, or a recurring donation? [![Ko-Fi][kofi-badge]][kofi] - No coffee, no code. ## Features * Shell and platform agnostic * Easily configurable * The __most__ configurable prompt utility * Fast * Secondary prompt * Right prompt * Transient prompt ## Documentation [![Documentation][docs-badge]][docs] ## Reviews * [Repo review](https://repo-reviews.github.io//reviews/2023-06-21_TameWizard_JanDeDobbeleer_oh-my-posh) by [TameWizard](https://github.com/TameWizard) ## Thanks * [Chris Benti](https://github.com/chrisbenti/PS-Config) providing the first influence to start oh-my-posh * [Keith Dahlby](https://github.com/dahlbyk/posh-git) for creating posh-git and making life more enjoyable * [Robby Russell](https://github.com/ohmyzsh/ohmyzsh) for creating oh-my-zsh, without him this would probably not be here * [Janne Mareike Koschinski](https://github.com/justjanne) for providing information on how to get certain information using Go (and the amazing [README](https://github.com/justjanne/powerline-go)) * [Starship](https://github.com/starship/starship/blob/master/src/init/mod.rs) for doing great things [kraken]: https://img.shields.io/badge/GitKraken-Legendary%20Git%20Tools-teal?style=plastic&logo=gitkraken [kraken-ref]: https://www.gitkraken.com/invite/nQmDPR9D [swag-badge]: https://img.shields.io/badge/Swag-Get%20some!-blue [swag]: https://swag.ohmyposh.dev [github-badge]: https://img.shields.io/badge/-Sponsor-fafbfc?logo=GitHub%20Sponsors [github-sponsors]: https://github.com/sponsors/JanDeDobbeleer [kofi-badge]: https://img.shields.io/badge/Ko--fi-Buy%20me%20a%20coffee!-%2346b798.svg [kofi]: https://ko-fi.com/jandedobbeleer [docs-badge]: https://img.shields.io/badge/Docs-ohmyposh.dev-blue [docs]: https://ohmyposh.dev [release-badge]: https://img.shields.io/github/v/release/jandedobbeleer/oh-my-posh?label=Release [release]: https://github.com/JanDeDobbeleer/oh-my-posh/releases/latest [merge-conflict]: https://www.mergeconflict.fm/ [merge-conflict-logo]: https://media24.fireside.fm/file/fireside-images-2024/podcasts/images/0/02d84890-e58d-43eb-ab4c-26bcc8524289/cover_small.jpg?v=1 [sponsor-link]: https://buy.polar.sh/polar_cl_qnmZxboq1IDUJo03mk2Jue6ktqZrCXElnzH2s2xbV2R ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Supported Versions Only the latest [release][releases] is supported. ## Reporting a Vulnerability Vulnerabilities can be sent in via [email][email] to avoid publishing in the open. Oh My Posh does not have a bounty program, neither do we respond to bug bounties. For valid security concerns, you can expect a response within 48 hours, and credit is given once an acceptable fix is found and published. [releases]: https://github.com/JanDeDobbeleer/oh-my-posh/releases [email]: mailto:security@ohmyposh.dev ================================================ FILE: apm.lock.yaml ================================================ lockfile_version: '1' generated_at: '2026-03-15T08:59:17.859220+00:00' apm_version: 0.7.9 dependencies: - repo_url: JanDeDobbeleer/agentic host: github.com resolved_commit: dfc3f6e80eae907b2ad0562b072386034171d5fe virtual_path: skills/conventional-commit is_virtual: true package_type: claude_skill deployed_files: - .github/skills/conventional-commit - repo_url: JanDeDobbeleer/agentic host: github.com resolved_commit: dfc3f6e80eae907b2ad0562b072386034171d5fe virtual_path: skills/golang is_virtual: true package_type: claude_skill deployed_files: - .github/skills/golang - repo_url: JanDeDobbeleer/agentic host: github.com resolved_commit: dfc3f6e80eae907b2ad0562b072386034171d5fe virtual_path: skills/markdown is_virtual: true package_type: claude_skill deployed_files: - .github/skills/markdown - repo_url: JanDeDobbeleer/agentic host: github.com resolved_commit: dfc3f6e80eae907b2ad0562b072386034171d5fe virtual_path: skills/powershell is_virtual: true package_type: claude_skill deployed_files: - .github/skills/powershell ================================================ FILE: apm.yml ================================================ name: oh-my-posh version: 1.0.0 description: A prompt theme engine for any shell. dependencies: apm: - JanDeDobbeleer/agentic/skills/conventional-commit - JanDeDobbeleer/agentic/skills/golang - JanDeDobbeleer/agentic/skills/markdown - JanDeDobbeleer/agentic/skills/powershell - JanDeDobbeleer/agentic/skills/vale-user-facing-text ================================================ FILE: build/post.ps1 ================================================ # Description: Post build script to compress the themes and generate SHA256 hashes for all files in the dist folder # Compress all themes $compress = @{ Path = "../themes/*.omp.*" CompressionLevel = "Fastest" DestinationPath = "../src/dist/themes.zip" } Compress-Archive @compress # Generate SHA256 hashes for all files in the dist folder Get-ChildItem ./dist -Exclude *.yaml, *.sig | Get-Unique | Foreach-Object { $zipHash = Get-FileHash $_.FullName -Algorithm SHA256 $zipHash.Hash | Out-File -Encoding 'UTF8' "../src/dist/$($_.Name).sha256" } ================================================ FILE: build/pre.ps1 ================================================ Param ( [string] $Version, [parameter(Mandatory = $false)] [string] $SDKVersion = "10.0.26100.0" ) git config --global user.name "GitHub Actions" git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" git tag $Version --force $PSDefaultParameterValues['Out-File:Encoding'] = 'UTF8' $shaSigningKeyLocation = Join-Path -Path $env:RUNNER_TEMP -ChildPath sha_signing_key.pem $env:SIGNING_KEY > $shaSigningKeyLocation Write-Output "SHA_SIGNING_KEY_LOCATION=$shaSigningKeyLocation" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append # install code signing dlib nuget.exe install Microsoft.Trusted.Signing.Client -Version 1.0.92 -ExcludeVersion -OutputDirectory $env:RUNNER_TEMP Write-Output "SIGNTOOLDLIB=$env:RUNNER_TEMP/Microsoft.Trusted.Signing.Client/bin/x64/Azure.CodeSigning.Dlib.dll" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append # requires Windows Dev Kit 10.0.26100.0 $signtool = "C:/Program Files (x86)/Windows Kits/10/bin/$SDKVersion/x64/signtool.exe" Write-Output "SIGNTOOL=$signtool" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append # openssl $openssl = 'C:/Program Files/Git/usr/bin/openssl.exe' Write-Output "OPENSSL=$openssl" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append ================================================ FILE: packages/msi/README.md ================================================ # MSI Package ## Prerequisites - [dotnet] - [wix]: `dotnet tool install --global wix` ## Build the package This guide assumes and advices the use of PowerShell as your shell environment for this purpose. ### Set the environment variables ```powershell $env:VERSION = "1.3.37" ``` ### Build the installer ```powershell wix build -arch arm64 -out install-arm64.msi ``` ## Install the package ### For the current user ```powershell install-arm64.msi ``` ### For all users ```powershell install-arm64.msi ALLUSERS=1 ``` [dotnet]: https://dotnet.microsoft.com/en-us/download/dotnet?cid=getdotnetcorecli [wix]: https://wixtoolset.org/docs/intro/ ================================================ FILE: packages/msi/appxmanifest.xml ================================================  Oh My Posh Jan Joris De Dobbeleer A prompt theme engine for any shell. icons\icon.png disabled ================================================ FILE: packages/msi/build.ps1 ================================================ <# .SYNOPSIS Builds MSI and MSIX packages for Oh My Posh. .DESCRIPTION This script creates MSI and MSIX installer packages for Oh My Posh with the specified architecture and version. It can optionally copy the executable, sign the packages, and generate hash files for verification. .PARAMETER Architecture The target architecture for the package. Must be either 'x64' or 'arm64'. .PARAMETER Version The version number to assign to the package (e.g., "1.2.3"). .PARAMETER SDKVersion The Windows SDK version to use for signing and packaging tools. Defaults to "10.0.26100.0". .PARAMETER Sign When specified, signs the MSI and MSIX packages using Azure Code Signing. .PARAMETER Copy When specified, copies the appropriate executable from the dist folder before packaging. .EXAMPLE .\build.ps1 -Architecture x64 -Version "1.2.3" -Copy Creates MSI and MSIX packages for x64 architecture with version 1.2.3, copying the executable first. .EXAMPLE .\build.ps1 -Architecture arm64 -Version "1.2.3" -Sign -Copy Creates and signs MSI and MSIX packages for arm64 architecture with version 1.2.3. .OUTPUTS Creates the following files in the 'out' directory: - install-{Architecture}.msi - install-{Architecture}.msix - Hash files (.sha256) for verification .NOTES Requires WiX toolset for MSI creation and Windows SDK for MSIX packaging and signing. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [ValidateSet('x64', 'arm64')] [string]$Architecture, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$Version, [Parameter()] [ValidateNotNullOrEmpty()] [string]$SDKVersion = "10.0.26100.0", [Parameter()] [switch]$Sign, [Parameter()] [switch]$Copy ) # Set error handling preferences $ErrorActionPreference = 'Stop' $PSNativeCommandUseErrorActionPreference = $true $PSDefaultParameterValues['Out-File:Encoding'] = 'UTF8' #region Helper Functions function Initialize-SigningEnvironment { <# .SYNOPSIS Sets up the signing environment and returns signing tool paths. .PARAMETER SDKVersion The Windows SDK version to use. .OUTPUTS Hashtable containing signtool and signtoolDlib paths. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$SDKVersion ) try { Write-Verbose "Setting up signing environment" -Verbose # Install Microsoft.Trusted.Signing.Client nuget.exe install Microsoft.Trusted.Signing.Client -Version 1.0.92 -x | Out-Null $signtoolDlib = "$PWD/Microsoft.Trusted.Signing.Client/bin/x64/Azure.CodeSigning.Dlib.dll" -replace '\\', '/' $signtool = "C:/Program Files (x86)/Windows Kits/10/bin/$SDKVersion/x64/signtool.exe" -replace '\\', '/' # Validate tools exist if (-not (Test-Path $signtool)) { throw "signtool.exe not found at: $signtool" } if (-not (Test-Path $signtoolDlib)) { throw "Azure.CodeSigning.Dlib.dll not found at: $signtoolDlib" } # Explicitly create and return a hashtable [hashtable]$result = @{ SignTool = $signtool SignToolDlib = $signtoolDlib } return $result } catch { Write-Error "Failed to initialize signing environment: $_" throw } } function Invoke-PackageSigning { <# .SYNOPSIS Signs a package using Azure Code Signing. .PARAMETER PackagePath The path to the package to sign. .PARAMETER SigningTools Hashtable containing signing tool paths from Initialize-SigningEnvironment. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [ValidateScript({Test-Path $_})] [string]$PackagePath, [Parameter(Mandatory = $true)] [hashtable]$SigningTools ) try { $packageName = Split-Path $PackagePath -Leaf Write-Verbose "Signing package: $packageName" -Verbose & $SigningTools.SignTool sign /v /debug /d "Oh My Posh" /fd SHA256 /tr 'http://timestamp.acs.microsoft.com' /td SHA256 /dlib $SigningTools.SignToolDlib /dmdf ../../src/metadata.json $PackagePath Write-Verbose "Successfully signed: $packageName" -Verbose } catch { Write-Error "Failed to sign package ${PackagePath}: ${_}" throw } } #endregion #region Main Script Write-Verbose "Building MSI for $Architecture with version $Version" -Verbose Write-Verbose "Setting up output directories" -Verbose try { New-Item -Path "." -Name "dist" -ItemType Directory -ErrorAction SilentlyContinue | Out-Null New-Item -Path "." -Name "out" -ItemType Directory -ErrorAction SilentlyContinue | Out-Null } catch { Write-Error "Failed to create output directories: ${_}" throw } if ($Copy) { $sourceFile = switch ($Architecture) { 'x64' { "posh-windows-amd64.exe" } Default { "posh-windows-$Architecture.exe" } } Write-Verbose "Copying $sourceFile to ./dist/oh-my-posh.exe" -Verbose try { $sourcePath = "../../dist/$sourceFile" if (-not (Test-Path $sourcePath)) { throw "Source file not found: $sourcePath" } Copy-Item -Path $sourcePath -Destination "./dist/oh-my-posh.exe" -Force } catch { Write-Error "Failed to copy executable: $_" throw } } # Set version environment variable for WiX $env:VERSION = $Version Write-Verbose "Creating MSI package" -Verbose try { # Define MSI package paths $msiFileName = "install-$Architecture.msi" $msiPackagePath = "$PWD/out/$msiFileName" -replace '\\', '/' Write-Verbose "Building MSI: $msiPackagePath" -Verbose wix build -arch $Architecture -out $msiPackagePath .\oh-my-posh.wxs if (-not (Test-Path $msiPackagePath)) { throw "MSI package was not created successfully" } } catch { Write-Error "Failed to create MSI package: ${_}" throw } if ($Sign) { $signingTools = Initialize-SigningEnvironment -SDKVersion $SDKVersion Invoke-PackageSigning -PackagePath $msiPackagePath -SigningTools $signingTools } Write-Verbose "Creating MSIX package" -Verbose try { # Define MSIX package paths and files $currentPath = $PWD -replace '\\', '/' $manifestPath = "$currentPath/appxmanifest.xml" $mappingFilePath = "$currentPath/mapping.txt" $msixPackagePath = "$currentPath/out/$($msiFileName)x" $makeappxPath = "C:/Program Files (x86)/Windows Kits/10/bin/$SDKVersion/x64/makeappx.exe" # Validate required files exist if (-not (Test-Path $manifestPath)) { throw "Manifest file not found: $manifestPath" } if (-not (Test-Path $mappingFilePath)) { throw "Mapping file not found: $mappingFilePath" } if (-not (Test-Path $makeappxPath)) { throw "makeappx.exe not found at: $makeappxPath" } # Update manifest with version and architecture [xml]$manifestDocument = Get-Content $manifestPath $manifestDocument.Package.Identity.Version = "$Version.0" $manifestDocument.Package.Identity.ProcessorArchitecture = $Architecture $manifestDocument.Save($manifestPath) # Build MSIX package Write-Verbose "Building MSIX: $msixPackagePath" -Verbose & "$makeappxPath" pack /p $msixPackagePath /v /o /m $manifestPath /f $mappingFilePath if (-not (Test-Path $msixPackagePath)) { throw "MSIX package was not created successfully" } } catch { Write-Error "Failed to create MSIX package: ${_}" throw } if ($Sign) { if ($null -eq $signingTools) { $signingTools = Initialize-SigningEnvironment -SDKVersion $SDKVersion } Invoke-PackageSigning -PackagePath $msixPackagePath -SigningTools $signingTools } Write-Verbose "Successfully completed building MSI and MSIX packages" -Verbose #endregion ================================================ FILE: packages/msi/dsc/oh-my-posh.config.dsc.resource.json ================================================ { "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.vscode.json", "description": "Allows configuring the Oh My Posh config files.", "export": { "executable": "oh-my-posh", "input": "stdin", "args": [ "config", "dsc", "export" ] }, "get": { "executable": "oh-my-posh", "input": "stdin", "args": [ "config", "dsc", "get" ] }, "schema": { "command": { "executable": "oh-my-posh", "args": [ "config", "dsc", "schema" ] } }, "set": { "executable": "oh-my-posh", "implementsPretest": true, "args": [ "config", "dsc", "set", { "jsonInputArg": "--state", "mandatory": true } ] }, "tags": [ "OhMyPosh", "linux", "macos", "windows", "shell", "powershell", "terminal", "theming", "configuration" ], "type": "OhMyPosh/Config", "version": "0.1.0" } ================================================ FILE: packages/msi/dsc/oh-my-posh.font.dsc.resource.json ================================================ { "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.vscode.json", "description": "Allows configuring the Oh My Posh font installs.", "export": { "executable": "oh-my-posh", "input": "stdin", "args": [ "font", "dsc", "export" ] }, "get": { "executable": "oh-my-posh", "input": "stdin", "args": [ "font", "dsc", "get" ] }, "schema": { "command": { "executable": "oh-my-posh", "args": [ "font", "dsc", "schema" ] } }, "set": { "executable": "oh-my-posh", "implementsPretest": true, "args": [ "font", "dsc", "set", { "jsonInputArg": "--state", "mandatory": true } ] }, "tags": [ "OhMyPosh", "linux", "macos", "windows", "powershell", "terminal", "theming", "fonts" ], "type": "OhMyPosh/Font", "version": "0.1.0" } ================================================ FILE: packages/msi/dsc/oh-my-posh.shell.dsc.resource.json ================================================ { "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.vscode.json", "description": "Allows configuring the Oh My Posh shell integration.", "export": { "executable": "oh-my-posh", "input": "stdin", "args": [ "shell", "dsc", "export" ] }, "get": { "executable": "oh-my-posh", "input": "stdin", "args": [ "shell", "dsc", "get" ] }, "schema": { "command": { "executable": "oh-my-posh", "args": [ "shell", "dsc", "schema" ] } }, "set": { "executable": "oh-my-posh", "implementsPretest": true, "args": [ "shell", "dsc", "set", { "jsonInputArg": "--state", "mandatory": true } ] }, "tags": [ "OhMyPosh", "linux", "macos", "windows", "shell", "powershell", "terminal", "theming" ], "type": "OhMyPosh/Shell", "version": "0.1.0" } ================================================ FILE: packages/msi/mapping.txt ================================================ [ResourceMetadata] "ResourceDimensions" "language-en-us" "ResourceId" "English" [Files] "./dist/oh-my-posh.exe" "oh-my-posh.exe" "./icons/icon.png" "/icons/icon.png" "./icons/44.png" "/icons/44.png" "./dsc/oh-my-posh.config.dsc.resource.json" "oh-my-posh.config.dsc.resource.json" "./dsc/oh-my-posh.shell.dsc.resource.json" "oh-my-posh.shell.dsc.resource.json" "./dsc/oh-my-posh.font.dsc.resource.json" "oh-my-posh.font.dsc.resource.json" "../../themes/1_shell.omp.json" "/themes/1_shell.omp.json" "../../themes/agnoster.minimal.omp.json" "/themes/agnoster.minimal.omp.json" "../../themes/agnoster.omp.json" "/themes/agnoster.omp.json" "../../themes/agnosterplus.omp.json" "/themes/agnosterplus.omp.json" "../../themes/aliens.omp.json" "/themes/aliens.omp.json" "../../themes/amro.omp.json" "/themes/amro.omp.json" "../../themes/atomic.omp.json" "/themes/atomic.omp.json" "../../themes/atomicBit.omp.json" "/themes/atomicBit.omp.json" "../../themes/avit.omp.json" "/themes/avit.omp.json" "../../themes/blue-owl.omp.json" "/themes/blue-owl.omp.json" "../../themes/blueish.omp.json" "/themes/blueish.omp.json" "../../themes/bubbles.omp.json" "/themes/bubbles.omp.json" "../../themes/bubblesextra.omp.json" "/themes/bubblesextra.omp.json" "../../themes/bubblesline.omp.json" "/themes/bubblesline.omp.json" "../../themes/capr4n.omp.json" "/themes/capr4n.omp.json" "../../themes/catppuccin.omp.json" "/themes/catppuccin.omp.json" "../../themes/catppuccin_frappe.omp.json" "/themes/catppuccin_frappe.omp.json" "../../themes/catppuccin_latte.omp.json" "/themes/catppuccin_latte.omp.json" "../../themes/catppuccin_macchiato.omp.json" "/themes/catppuccin_macchiato.omp.json" "../../themes/catppuccin_mocha.omp.json" "/themes/catppuccin_mocha.omp.json" "../../themes/cert.omp.json" "/themes/cert.omp.json" "../../themes/chips.omp.json" "/themes/chips.omp.json" "../../themes/cinnamon.omp.json" "/themes/cinnamon.omp.json" "../../themes/clean-detailed.omp.json" "/themes/clean-detailed.omp.json" "../../themes/cloud-context.omp.json" "/themes/cloud-context.omp.json" "../../themes/cloud-native-azure.omp.json" "/themes/cloud-native-azure.omp.json" "../../themes/cobalt2.omp.json" "/themes/cobalt2.omp.json" "../../themes/craver.omp.json" "/themes/craver.omp.json" "../../themes/darkblood.omp.json" "/themes/darkblood.omp.json" "../../themes/devious-diamonds.omp.yaml" "/themes/devious-diamonds.omp.yaml" "../../themes/di4am0nd.omp.json" "/themes/di4am0nd.omp.json" "../../themes/dracula.omp.json" "/themes/dracula.omp.json" "../../themes/easy-term.omp.json" "/themes/easy-term.omp.json" "../../themes/emodipt-extend.omp.json" "/themes/emodipt-extend.omp.json" "../../themes/emodipt.omp.json" "/themes/emodipt.omp.json" "../../themes/fish.omp.json" "/themes/fish.omp.json" "../../themes/free-ukraine.omp.json" "/themes/free-ukraine.omp.json" "../../themes/froczh.omp.json" "/themes/froczh.omp.json" "../../themes/glowsticks.omp.yaml" "/themes/glowsticks.omp.yaml" "../../themes/gmay.omp.json" "/themes/gmay.omp.json" "../../themes/grandpa-style.omp.json" "/themes/grandpa-style.omp.json" "../../themes/gruvbox.omp.json" "/themes/gruvbox.omp.json" "../../themes/half-life.omp.json" "/themes/half-life.omp.json" "../../themes/honukai.omp.json" "/themes/honukai.omp.json" "../../themes/hotstick.minimal.omp.json" "/themes/hotstick.minimal.omp.json" "../../themes/hul10.omp.json" "/themes/hul10.omp.json" "../../themes/hunk.omp.json" "/themes/hunk.omp.json" "../../themes/huvix.omp.json" "/themes/huvix.omp.json" "../../themes/if_tea.omp.json" "/themes/if_tea.omp.json" "../../themes/illusi0n.omp.json" "/themes/illusi0n.omp.json" "../../themes/iterm2.omp.json" "/themes/iterm2.omp.json" "../../themes/jandedobbeleer.omp.json" "/themes/jandedobbeleer.omp.json" "../../themes/jblab_2021.omp.json" "/themes/jblab_2021.omp.json" "../../themes/jonnychipz.omp.json" "/themes/jonnychipz.omp.json" "../../themes/json.omp.json" "/themes/json.omp.json" "../../themes/jtracey93.omp.json" "/themes/jtracey93.omp.json" "../../themes/jv_sitecorian.omp.json" "/themes/jv_sitecorian.omp.json" "../../themes/kali.omp.json" "/themes/kali.omp.json" "../../themes/kushal.omp.json" "/themes/kushal.omp.json" "../../themes/lambda.omp.json" "/themes/lambda.omp.json" "../../themes/lambdageneration.omp.json" "/themes/lambdageneration.omp.json" "../../themes/larserikfinholt.omp.json" "/themes/larserikfinholt.omp.json" "../../themes/lightgreen.omp.json" "/themes/lightgreen.omp.json" "../../themes/M365Princess.omp.json" "/themes/M365Princess.omp.json" "../../themes/marcduiker.omp.json" "/themes/marcduiker.omp.json" "../../themes/markbull.omp.json" "/themes/markbull.omp.json" "../../themes/material.omp.json" "/themes/material.omp.json" "../../themes/microverse-power.omp.json" "/themes/microverse-power.omp.json" "../../themes/mojada.omp.json" "/themes/mojada.omp.json" "../../themes/montys.omp.json" "/themes/montys.omp.json" "../../themes/mt.omp.json" "/themes/mt.omp.json" "../../themes/multiverse-neon.omp.json" "/themes/multiverse-neon.omp.json" "../../themes/negligible.omp.json" "/themes/negligible.omp.json" "../../themes/neko.omp.json" "/themes/neko.omp.json" "../../themes/night-owl.omp.json" "/themes/night-owl.omp.json" "../../themes/nordtron.omp.json" "/themes/nordtron.omp.json" "../../themes/nu4a.omp.json" "/themes/nu4a.omp.json" "../../themes/onehalf.minimal.omp.json" "/themes/onehalf.minimal.omp.json" "../../themes/paradox.omp.json" "/themes/paradox.omp.json" "../../themes/pararussel.omp.json" "/themes/pararussel.omp.json" "../../themes/patriksvensson.omp.json" "/themes/patriksvensson.omp.json" "../../themes/peru.omp.json" "/themes/peru.omp.json" "../../themes/pixelrobots.omp.json" "/themes/pixelrobots.omp.json" "../../themes/plague.omp.json" "/themes/plague.omp.json" "../../themes/poshmon.omp.json" "/themes/poshmon.omp.json" "../../themes/powerlevel10k_classic.omp.json" "/themes/powerlevel10k_classic.omp.json" "../../themes/powerlevel10k_lean.omp.json" "/themes/powerlevel10k_lean.omp.json" "../../themes/powerlevel10k_modern.omp.json" "/themes/powerlevel10k_modern.omp.json" "../../themes/powerlevel10k_rainbow.omp.json" "/themes/powerlevel10k_rainbow.omp.json" "../../themes/powerline.omp.json" "/themes/powerline.omp.json" "../../themes/probua.minimal.omp.json" "/themes/probua.minimal.omp.json" "../../themes/pure.omp.json" "/themes/pure.omp.json" "../../themes/quick-term.omp.json" "/themes/quick-term.omp.json" "../../themes/remk.omp.json" "/themes/remk.omp.json" "../../themes/robbyrussell.omp.json" "/themes/robbyrussell.omp.json" "../../themes/rudolfs-dark.omp.json" "/themes/rudolfs-dark.omp.json" "../../themes/rudolfs-light.omp.json" "/themes/rudolfs-light.omp.json" "../../themes/sim-web.omp.json" "/themes/sim-web.omp.json" "../../themes/slim.omp.json" "/themes/slim.omp.json" "../../themes/slimfat.omp.json" "/themes/slimfat.omp.json" "../../themes/smoothie.omp.json" "/themes/smoothie.omp.json" "../../themes/sonicboom_dark.omp.json" "/themes/sonicboom_dark.omp.json" "../../themes/sonicboom_light.omp.json" "/themes/sonicboom_light.omp.json" "../../themes/sorin.omp.json" "/themes/sorin.omp.json" "../../themes/space.omp.json" "/themes/space.omp.json" "../../themes/spaceship.omp.json" "/themes/spaceship.omp.json" "../../themes/star.omp.json" "/themes/star.omp.json" "../../themes/stelbent-compact.minimal.omp.json" "/themes/stelbent-compact.minimal.omp.json" "../../themes/stelbent.minimal.omp.json" "/themes/stelbent.minimal.omp.json" "../../themes/takuya.omp.json" "/themes/takuya.omp.json" "../../themes/the-unnamed.omp.json" "/themes/the-unnamed.omp.json" "../../themes/thecyberden.omp.json" "/themes/thecyberden.omp.json" "../../themes/tiwahu.omp.json" "/themes/tiwahu.omp.json" "../../themes/tokyo.omp.json" "/themes/tokyo.omp.json" "../../themes/tokyonight_storm.omp.json" "/themes/tokyonight_storm.omp.json" "../../themes/tonybaloney.omp.json" "/themes/tonybaloney.omp.json" "../../themes/uew.omp.json" "/themes/uew.omp.json" "../../themes/unicorn.omp.json" "/themes/unicorn.omp.json" "../../themes/velvet.omp.json" "/themes/velvet.omp.json" "../../themes/wholespace.omp.json" "/themes/wholespace.omp.json" "../../themes/wopian.omp.json" "/themes/wopian.omp.json" "../../themes/xtoys.omp.json" "/themes/xtoys.omp.json" "../../themes/ys.omp.json" "/themes/ys.omp.json" "../../themes/zash.omp.json" "/themes/zash.omp.json" ================================================ FILE: packages/msi/oh-my-posh.wxs ================================================ ================================================ FILE: src/.golangci.yml ================================================ version: "2" run: allow-parallel-runners: true linters: default: none enable: - bodyclose - copyloopvar - dupl - errcheck - exhaustive - goconst - gocritic - gocyclo - goprintffuncname - govet - ineffassign - lll - misspell - nakedret - noctx - nolintlint - revive - rowserrcheck - staticcheck - unconvert - unparam - unused - whitespace settings: gocritic: enabled-tags: - diagnostic - opinionated - performance - style disabled-tags: - experimental lll: line-length: 180 revive: rules: - name: var-naming disabled: true exclusions: generated: lax presets: - comments - common-false-positives - legacy - std-error-handling paths: - third_party$ - builtin$ - examples$ formatters: enable: - gofmt - goimports exclusions: generated: lax paths: - third_party$ - builtin$ - examples$ ================================================ FILE: src/.goreleaser.yml ================================================ # Make sure to check the documentation at https://goreleaser.com # yaml-language-server: $schema=https://goreleaser.com/static/schema.json version: 2 before: hooks: - go mod tidy - go install github.com/tc-hib/go-winres@latest - go-winres make --product-version=git-tag --file-version=git-tag --arch="amd64,arm64" builds: - binary: "posh-{{ .Os }}-{{ .Arch }}" no_unique_dist_dir: true flags: - -a ldflags: - -s -w - -X github.com/jandedobbeleer/oh-my-posh/src/build.Version={{ .Version }} - -X github.com/jandedobbeleer/oh-my-posh/src/build.Date={{ .Date }} - -extldflags "-static" tags: - netgo - osusergo - static_build - timetzdata env: - CGO_ENABLED=0 - GOEXPERIMENT=greenteagc,jsonv2 goos: - linux - windows - darwin - freebsd goarch: - amd64 - arm64 - arm ignore: - goos: darwin goarch: arm - goos: windows goarch: arm hooks: post: - pwsh -c "if ('{{ .Path }}'.EndsWith('.exe')) { & '{{ .Env.SIGNTOOL }}' sign /v /debug /fd SHA256 /tr 'http://timestamp.acs.microsoft.com' /td SHA256 /dlib '{{ .Env.SIGNTOOLDLIB }}' /dmdf './metadata.json' '{{ .Path }}' }" archives: - id: oh-my-posh format: binary name_template: "posh-{{ .Os }}-{{ .Arch }}" checksum: name_template: 'checksums.txt' signs: - cmd: pwsh args: - "-c" - "& '{{ .Env.OPENSSL }}' pkeyutl -sign -inkey '{{ .Env.SHA_SIGNING_KEY_LOCATION }}' -out '${artifact}.sig' -rawin -in '${artifact}'" artifacts: checksum changelog: disable: true ================================================ FILE: src/build/version.go ================================================ package build var ( Date string Version = "0.0.0-dev" ) ================================================ FILE: src/cache/cache.go ================================================ package cache import ( "encoding/gob" "time" ) func init() { gob.Register(&Entry[any]{}) gob.Register(Template{}) gob.Register(SimpleTemplate{}) gob.Register((*Duration)(nil)) gob.Register(map[string]bool{}) } const ( DeviceStore = "omp.cache" ) const ( TEMPLATECACHE = "template_cache" TOGGLECACHE = "toggle_cache" PROMPTCOUNTCACHE = "prompt_count_cache" ENGINECACHE = "engine_cache" FONTLISTCACHE = "font_list_cache" CLAUDECACHE = "claude_cache" ) type Entry[T any] struct { Value T Timestamp int64 TTL int } func (c *Entry[T]) Expired() bool { if c.TTL < 0 { return false } return time.Now().Unix() >= (c.Timestamp + int64(c.TTL)) } ================================================ FILE: src/cache/clear.go ================================================ package cache import ( "os" "path/filepath" "slices" "strings" "time" "github.com/jandedobbeleer/oh-my-posh/src/log" ) // Clear removes cache files from the cache directory. // // If force is true, the entire cache directory is removed. // If force is false, only cache files older than 7 days that match certain patterns are deleted. // The excludedFiles parameter allows you to specify file names that should not be deleted, // even if they would otherwise be eligible for removal. func Clear(force bool, excludedFiles ...string) error { defer log.Trace(time.Now()) if force { return os.RemoveAll(Path()) } // get all files in the cache directory that start with omp.cache and delete them files, err := os.ReadDir(Path()) if err != nil { return err } // get all log files as well if logFiles, err := os.ReadDir(filepath.Join(Path(), "logs")); err == nil { files = append(files, logFiles...) } shouldSkip := func(fileName string) bool { if slices.Contains(excludedFiles, fileName) { return true } return strings.EqualFold(fileName, DeviceStore) || strings.HasPrefix(fileName, "init.") } if len(excludedFiles) > 0 { log.Debug("excluding files from deletion:", strings.Join(excludedFiles, ", ")) } deleteFile := func(file string) { path := filepath.Join(Path(), file) err := os.Remove(path) if err != nil { log.Error(err) return } log.Debugf("removed cache file: %s", path) } cacheTTL := GetTTL() log.Debugf("removing cache files older than %d days", cacheTTL) for _, file := range files { if file.IsDir() { continue } if shouldSkip(file.Name()) { log.Debug("skipping excluded file:", file.Name()) continue } cacheFileInfo, err := file.Info() if err != nil { log.Debug("skipping file, cannot get info:", file.Name()) continue } if cacheFileInfo.ModTime().After(time.Now().AddDate(0, 0, -cacheTTL)) { log.Debug("skipping recently used file:", file.Name()) continue } deleteFile(file.Name()) } return nil } func GetTTL() int { cacheTTL, OK := Get[int](Device, TTL) if !OK || cacheTTL <= 0 { cacheTTL = 7 } return cacheTTL } ================================================ FILE: src/cache/command.go ================================================ package cache import ( "github.com/jandedobbeleer/oh-my-posh/src/maps" ) type Command struct { Commands *maps.Concurrent[string] } func (c *Command) Set(command, path string) { c.Commands.Set(command, path) } func (c *Command) Get(command string) (string, bool) { cacheCommand, found := c.Commands.Get(command) if !found { return "", false } return cacheCommand, true } ================================================ FILE: src/cache/duration.go ================================================ package cache import ( "time" ) type Duration string const ( INFINITE = Duration("infinite") NONE = Duration("none") ONEWEEK = Duration("168h") ONEDAY = Duration("24h") TWOYEARS = Duration("17520h") ) func (d Duration) Seconds() int { if d == NONE { return 0 } if d == INFINITE { return -1 } duration, err := time.ParseDuration(string(d)) if err != nil { return 0 } return int(duration.Seconds()) } func (d Duration) IsEmpty() bool { return d == "" } func ToDuration(seconds int) Duration { if seconds == 0 { return "" } if seconds == -1 { return INFINITE } duration := time.Duration(seconds) * time.Second return Duration(duration.String()) } ================================================ FILE: src/cache/duration_test.go ================================================ package cache import ( "testing" "github.com/stretchr/testify/assert" ) func TestSeconds(t *testing.T) { cases := []struct { Case string Duration Duration Expected int }{ { Case: "2 seconds", Duration: "2s", Expected: 2, }, { Case: "1 minute", Duration: "1m", Expected: 60, }, { Case: "2 hours", Duration: "2h", Expected: 7200, }, { Case: "2 days", Duration: "48h", Expected: 172800, }, { Case: "invalid", Duration: "foo", Expected: 0, }, { Case: "1 fortnight", Duration: "1fortnight", Expected: 0, }, { Case: "infinite", Duration: "infinite", Expected: -1, }, } for _, tc := range cases { got := tc.Duration.Seconds() assert.Equal(t, tc.Expected, got, tc.Case) } } ================================================ FILE: src/cache/file_map_windows.go ================================================ package cache import ( "fmt" "syscall" "unsafe" "github.com/jandedobbeleer/oh-my-posh/src/log" ) // Configuration constants const ( minStringSize = 50 * 1024 // 50KB minimum string size maxStringSize = 10 * 1024 * 1024 // 10MB maximum string size ) // Windows API constants const ( fileMapAllAccess = 0x001f001f pageReadwrite = 0x04 genericRead = 0x80000000 genericWrite = 0x40000000 createAlways = 2 openExisting = 3 fileAttributeNormal = 0x80 ) // Windows API functions var ( kernel32 = syscall.NewLazyDLL("kernel32.dll") createFileW = kernel32.NewProc("CreateFileW") createFileMappingW = kernel32.NewProc("CreateFileMappingW") mapViewOfFile = kernel32.NewProc("MapViewOfFile") unmapViewOfFile = kernel32.NewProc("UnmapViewOfFile") closeHandle = kernel32.NewProc("CloseHandle") setFilePointer = kernel32.NewProc("SetFilePointer") setEndOfFile = kernel32.NewProc("SetEndOfFile") getFileSizeEx = kernel32.NewProc("GetFileSizeEx") ) // PersistentSharedString represents a memory-mapped file for storing a single string type PersistentSharedString struct { filePath string fileHandle uintptr mapHandle uintptr data uintptr size int // Current allocated size } func createOrOpenPersistentString(filePath string) (*PersistentSharedString, error) { return createOrOpenPersistentStringWithSize(filePath, minStringSize) } func createOrOpenPersistentStringWithSize(filePath string, requiredSize int) (*PersistentSharedString, error) { // Ensure size is within bounds if requiredSize < minStringSize { requiredSize = minStringSize } if requiredSize > maxStringSize { return nil, fmt.Errorf("required size %d exceeds maximum %d", requiredSize, maxStringSize) } // First, try to open existing file pss, err := openExistingFileWithSize(filePath, requiredSize) if err == nil { return pss, nil } // File doesn't exist or too small, create new one with required size return createNewFileWithSize(filePath, requiredSize) } // openExistingFileWithSize attempts to open an existing memory-mapped file // openExistingFileWithSize attempts to open an existing memory-mapped file func openExistingFileWithSize(filePath string, requiredSize int) (*PersistentSharedString, error) { filePathPtr, err := syscall.UTF16PtrFromString(filePath) if err != nil { return nil, fmt.Errorf("failed to convert file path to UTF16: %v", err) } // Try to open existing file fileHandle, _, _ := createFileW.Call( uintptr(unsafe.Pointer(filePathPtr)), // lpFileName genericRead|genericWrite, // dwDesiredAccess 0, // dwShareMode 0, // lpSecurityAttributes openExisting, // dwCreationDisposition fileAttributeNormal, // dwFlagsAndAttributes 0, // hTemplateFile ) if fileHandle == uintptr(0xFFFFFFFFFFFFFFFF) { // INVALID_HANDLE_VALUE return nil, fmt.Errorf("file does not exist") } // Get file size to check if it's large enough var fileSize int64 ret, _, _ := getFileSizeEx.Call(fileHandle, uintptr(unsafe.Pointer(&fileSize))) if ret == 0 { _, _, _ = closeHandle.Call(fileHandle) return nil, fmt.Errorf("failed to get file size") } actualSize := int(fileSize) - 5 // Subtract header (4 bytes length + 1 null terminator) if actualSize < requiredSize { // Existing file is too small, close and recreate _, _, _ = closeHandle.Call(fileHandle) return nil, fmt.Errorf("existing file is too small (%d < %d)", actualSize, requiredSize) } return createMappingFromFileWithSize(filePath, fileHandle, actualSize) } // createNewFileWithSize creates a new memory-mapped file with the specified size func createNewFileWithSize(filePath string, size int) (*PersistentSharedString, error) { filePathPtr, err := syscall.UTF16PtrFromString(filePath) if err != nil { return nil, fmt.Errorf("failed to convert file path to UTF16: %v", err) } // Create new file fileHandle, _, err := createFileW.Call( uintptr(unsafe.Pointer(filePathPtr)), // lpFileName genericRead|genericWrite, // dwDesiredAccess 0, // dwShareMode 0, // lpSecurityAttributes createAlways, // dwCreationDisposition (overwrites if exists) fileAttributeNormal, // dwFlagsAndAttributes 0, // hTemplateFile ) if fileHandle == uintptr(0xFFFFFFFFFFFFFFFF) { // INVALID_HANDLE_VALUE return nil, fmt.Errorf("CreateFileW failed: %v", err) } // Set file size (4 bytes for length + size for string + 1 for null terminator) totalSize := size + 5 _, _, _ = setFilePointer.Call(fileHandle, uintptr(totalSize), 0, 0) // FILE_BEGIN = 0 _, _, _ = setEndOfFile.Call(fileHandle) pss, mapErr := createMappingFromFileWithSize(filePath, fileHandle, size) if mapErr != nil { _, _, _ = closeHandle.Call(fileHandle) return nil, mapErr } // Initialize new file with empty string basePtr := unsafe.Pointer(pss.data) lengthPtr := (*uint32)(basePtr) *lengthPtr = 0 return pss, nil } // createMappingFromFileWithSize creates a memory mapping from an open file handle with specified size func createMappingFromFileWithSize(filePath string, fileHandle uintptr, size int) (*PersistentSharedString, error) { totalSize := size + 5 // 4 bytes length + size + 1 null terminator // Create file mapping mapHandle, _, err := createFileMappingW.Call( fileHandle, // hFile 0, // lpAttributes (NULL) pageReadwrite, // flProtect 0, // dwMaximumSizeHigh uintptr(totalSize), // dwMaximumSizeLow 0, // lpName (NULL for unnamed mapping) ) if mapHandle == 0 { return nil, fmt.Errorf("CreateFileMappingW failed: %v", err) } // Map view of file data, _, err := mapViewOfFile.Call( mapHandle, // hFileMappingObject fileMapAllAccess, // dwDesiredAccess 0, // dwFileOffsetHigh 0, // dwFileOffsetLow uintptr(totalSize), // dwNumberOfBytesToMap ) if data == 0 { _, _, _ = closeHandle.Call(mapHandle) return nil, fmt.Errorf("MapViewOfFile failed: %v", err) } return &PersistentSharedString{ filePath: filePath, fileHandle: fileHandle, mapHandle: mapHandle, data: data, size: size, }, nil } // SetString stores a string in the memory-mapped file (automatically persisted) func (pss *PersistentSharedString) SetString(value string) error { strBytes := []byte(value) if len(strBytes) > pss.size { return fmt.Errorf("string too large for allocated space (%d > %d)", len(strBytes), pss.size) } basePtr := unsafe.Pointer(pss.data) // Write length as first 4 bytes (little-endian) lengthPtr := (*uint32)(basePtr) *lengthPtr = uint32(len(strBytes)) // Write string data starting at offset 4 if len(strBytes) > 0 { stringPtr := unsafe.Add(basePtr, 4) stringSlice := unsafe.Slice((*byte)(stringPtr), len(strBytes)) copy(stringSlice, strBytes) } // Write null terminator nullPtr := (*byte)(unsafe.Add(basePtr, 4+len(strBytes))) *nullPtr = 0 // No need to explicitly flush - Windows handles this automatically return nil } func (pss *PersistentSharedString) bytes() []byte { basePtr := unsafe.Pointer(pss.data) // Read length from first 4 bytes lengthPtr := (*uint32)(basePtr) length := *lengthPtr if length == 0 { log.Debug("empty string") return []byte{0} } if length > uint32(pss.size) { log.Error(fmt.Errorf("corrupted data: length %d exceeds allocated size %d", length, pss.size)) return []byte{0} } // Read string data starting at offset 4 stringPtr := unsafe.Add(basePtr, 4) stringSlice := unsafe.Slice((*byte)(stringPtr), length) // Convert to string result := make([]byte, length) copy(result, stringSlice) return result } // Close closes the memory-mapped file and handles func (pss *PersistentSharedString) close() error { var err error if pss.data != 0 { if ret, _, e := unmapViewOfFile.Call(pss.data); ret == 0 { err = fmt.Errorf("UnmapViewOfFile failed: %v", e) } pss.data = 0 } if pss.mapHandle != 0 { if ret, _, e := closeHandle.Call(pss.mapHandle); ret == 0 { if err == nil { err = fmt.Errorf("CloseHandle (mapping) failed: %v", e) } } pss.mapHandle = 0 } if pss.fileHandle != 0 { if ret, _, e := closeHandle.Call(pss.fileHandle); ret == 0 { if err == nil { err = fmt.Errorf("CloseHandle (file) failed: %v", e) } } pss.fileHandle = 0 } return err } ================================================ FILE: src/cache/file_unix.go ================================================ //go:build !windows package cache import ( "io" "os" ) func openFile(filePath string) (io.ReadWriteCloser, error) { return os.OpenFile(filePath, os.O_CREATE|os.O_RDWR, 0o644) } ================================================ FILE: src/cache/file_windows.go ================================================ package cache import ( "bytes" "fmt" "io" "github.com/jandedobbeleer/oh-my-posh/src/log" ) // persistentStringRWCloser implements io.ReadWriteCloser for PersistentSharedString type persistentStringRWCloser struct { pss *PersistentSharedString buf *bytes.Buffer filePath string dirty bool } func NewPersistentStringRWCloser(pss *PersistentSharedString) io.ReadWriteCloser { return &persistentStringRWCloser{ pss: pss, buf: bytes.NewBuffer(pss.bytes()), filePath: pss.filePath, } } func (rw *persistentStringRWCloser) Read(p []byte) (int, error) { return rw.buf.Read(p) } func (rw *persistentStringRWCloser) Write(p []byte) (int, error) { if !rw.dirty { rw.buf.Reset() rw.dirty = true } return rw.buf.Write(p) } func (rw *persistentStringRWCloser) Close() error { defer rw.pss.close() if !rw.dirty { return nil } data := rw.buf.String() dataSize := len(data) // Check if the data fits in the current allocation if dataSize <= rw.pss.size { return rw.pss.SetString(data) } // Data is too large, need to recreate with larger size log.Debugf("cache data size (%d) exceeds current allocation (%d), recreating file", dataSize, rw.pss.size) // Calculate new size with some growth factor (1.5x) to reduce future reallocations newSize := max(dataSize+(dataSize/2), minStringSize) if newSize > maxStringSize { return fmt.Errorf("required cache size %d exceeds maximum %d", dataSize, maxStringSize) } // Close current mapping before recreating if err := rw.pss.close(); err != nil { log.Error(err) } // Create new file with larger size newPss, err := createOrOpenPersistentStringWithSize(rw.filePath, newSize) if err != nil { return fmt.Errorf("failed to recreate cache file with size %d: %v", newSize, err) } // Write the data to the new file return newPss.SetString(data) } func openFile(filePath string) (io.ReadWriteCloser, error) { pss, err := createOrOpenPersistentString(filePath) if err != nil { log.Error(err) return nil, err } return NewPersistentStringRWCloser(pss), nil } ================================================ FILE: src/cache/init.go ================================================ package cache import ( "fmt" "os" "sync" "time" "github.com/google/uuid" "github.com/jandedobbeleer/oh-my-posh/src/log" ) type Option func() var ( sessionID string newSession bool persist bool noSession bool once sync.Once ) var NewSession Option = func() { log.Debug("starting a new session") newSession = true } var Persist Option = func() { log.Debug("enable persistent cache") persist = true } var NoSession Option = func() { log.Debug("disable session cache") noSession = true } func Init(shell string, options ...Option) { for _, opt := range options { opt() } Device.init(DeviceStore, persist) if noSession { return } sessionFileName := fmt.Sprintf("%s.%s.%s", shell, SessionID(), DeviceStore) Session.init(sessionFileName, persist) } func SessionID() string { defer log.Trace(time.Now()) once.Do(func() { if newSession { sessionID = uuid.NewString() return } sessionID = os.Getenv("POSH_SESSION_ID") if sessionID == "" { sessionID = uuid.NewString() } }) return sessionID } func Close() { Session.close() Device.close() } ================================================ FILE: src/cache/path.go ================================================ package cache import ( "os" "path/filepath" "time" "github.com/jandedobbeleer/oh-my-posh/src/log" "github.com/jandedobbeleer/oh-my-posh/src/runtime/path" ) var cachePath string func Path() string { defer log.Trace(time.Now()) if cachePath != "" { return cachePath } var OK bool // allow the user to set the cache path using OMP_CACHE_DIR if cachePath, OK = returnOrBuildCachePath(os.Getenv("OMP_CACHE_DIR")); OK { return cachePath } if cachePath, OK = platformCachePath(); OK { return cachePath } // try to create the cache folder in the user's home directory if non-existent dotCache := filepath.Join(path.Home(), ".cache") if _, err := os.Stat(dotCache); err != nil { _ = os.Mkdir(dotCache, 0o755) } // HOME cache folder if cachePath, OK = returnOrBuildCachePath(dotCache); OK { return cachePath } return cachePath } func returnOrBuildCachePath(input string) (string, bool) { // validate root path if _, err := os.Stat(input); err != nil { return "", false } // validate oh-my-posh folder, if non existent, create it cachePath := filepath.Join(input, "oh-my-posh") if _, err := os.Stat(cachePath); err == nil { return cachePath, true } if err := os.Mkdir(cachePath, 0o755); err != nil { return "", false } return cachePath, true } ================================================ FILE: src/cache/path_unix.go ================================================ //go:build !windows package cache import "os" func platformCachePath() (string, bool) { if cachePath, OK := returnOrBuildCachePath(os.Getenv("XDG_CACHE_HOME")); OK { return cachePath, true } return "", false } func PackageFamilyName() (string, bool) { return "", false } ================================================ FILE: src/cache/path_windows.go ================================================ package cache import ( "os" "path/filepath" "syscall" "time" "unsafe" "github.com/jandedobbeleer/oh-my-posh/src/log" ) func platformCachePath() (string, bool) { if pfn, OK := PackageFamilyName(); OK { // WINDOWS MSIX cache folder, will only be present when oh-my-posh is installed via MSIX msixLocalAppData := filepath.Join(os.Getenv("LOCALAPPDATA"), "Packages", pfn, "LocalCache", "Local") if cachePath, OK := returnOrBuildCachePath(msixLocalAppData); OK { return cachePath, true } } // WINDOWS cache folder, should not exist elsewhere if cachePath, OK := returnOrBuildCachePath(os.Getenv("LOCALAPPDATA")); OK { return cachePath, true } return "", false } func PackageFamilyName() (string, bool) { defer log.Trace(time.Now()) kernel32 := syscall.NewLazyDLL("kernel32.dll") procGetCurrentPackageFamilyName := kernel32.NewProc("GetCurrentPackageFamilyName") var length uint32 = 256 buf := make([]uint16, length) ret, _, _ := procGetCurrentPackageFamilyName.Call( uintptr(unsafe.Pointer(&length)), uintptr(unsafe.Pointer(&buf[0])), ) if ret != 0 { log.Debug("failed to get PackageFamilyName") return "", false } pfn := syscall.UTF16ToString(buf) log.Debug("PackageFamilyName:", pfn) return pfn, true } ================================================ FILE: src/cache/store.go ================================================ package cache import ( "encoding/gob" "fmt" "os" "path/filepath" "strings" "time" "github.com/jandedobbeleer/oh-my-posh/src/log" "github.com/jandedobbeleer/oh-my-posh/src/maps" ) type store struct { cache *maps.Concurrent[*Entry[any]] filePath string dirty bool persist bool } var ( session *store device *store ) type Store string const ( Session Store = "session" Device Store = "device" TTL string = "ttl" ) func (s Store) new() *store { return &store{ cache: maps.NewConcurrent[*Entry[any]](), } } // getStore returns the appropriate store based on the Store identifier func (s Store) get() *store { switch s { //nolint:exhaustive case Device: if device == nil { device = s.new() } return device default: if session == nil { session = s.new() } return session } } // Init initializes a store with the given file path func (s Store) init(filePath string, persist bool) { defer log.Trace(time.Now(), string(s), filePath) store := s.get() store.cache = maps.NewConcurrent[*Entry[any]]() store.filePath = filepath.Join(Path(), filePath) store.persist = persist reader, err := openFile(store.filePath) if err != nil { // set to dirty so we create it on close log.Error(err) store.dirty = true return } defer reader.Close() var list maps.Simple[*Entry[any]] dec := gob.NewDecoder(reader) if err := dec.Decode(&list); err != nil { log.Error(err) // If gob decoding fails, the cache file might be from the old format // Set dirty to true so we recreate it in gob format store.dirty = true return } for key, entry := range list { if entry.Expired() { log.Debugf("(%s) skipping expired key: %s", string(s), key) continue } log.Debugf("(%s) loading %s", string(s), key) store.cache.Set(key, entry) } } // touchSessionFile updates the session file's modification time if it's older than 1 hour. // This prevents stale session cache files from being cleaned up while reducing steady-state overhead. func touchSessionFile(filePath string) { info, err := os.Stat(filePath) if err != nil { return } if time.Since(info.ModTime()) <= time.Hour { return } if err := os.Chtimes(filePath, time.Now(), time.Now()); err != nil { log.Error(err) } } func (s Store) close() { defer log.Trace(time.Now(), string(s)) store := s.get() if store == nil || !store.persist || !store.dirty { if s == Session && store != nil && store.filePath != "" { touchSessionFile(store.filePath) } log.Debugf("(%s) not persisting", string(s)) return } cache := store.cache.ToSimple() file, err := openFile(store.filePath) if err != nil { log.Error(err) return } defer func() { if err := file.Close(); err != nil { log.Error(err) } }() enc := gob.NewEncoder(file) if err := enc.Encode(cache); err != nil { log.Error(err) } } // Get retrieves a typed value from the specified store func Get[T any](s Store, key string) (T, bool) { var zero T defer log.Trace(time.Now(), string(s), key) store := s.get() if store == nil { log.Debugf("(%s) store is nil", string(s)) return zero, false } entry, found := store.cache.Get(key) if !found { log.Debugf("(%s) key not found: %s", string(s), key) return zero, false } if entry.Expired() { log.Debugf("(%s) key expired: %s", string(s), key) store.cache.Delete(key) store.dirty = true return zero, false } // Type assertion to get the typed value if typed, ok := entry.Value.(T); ok { log.Debugf("(%s) found entry: %s - %v", string(s), key, typed) return typed, true } log.Error(fmt.Errorf("(%s) type mismatch for key: %s. Got %T, expected %T", string(s), key, entry.Value, zero)) return zero, false } // Set stores a typed value in the specified store func Set[T any](s Store, key string, value T, duration Duration) { defer log.Trace(time.Now(), string(s), key) store := s.get() if store == nil { log.Debugf("(%s) store is nil", string(s)) return } seconds := duration.Seconds() if seconds == 0 { return } log.Debugf("(%s) setting entry: %s - %v with duration: %s", string(s), key, value, string(duration)) store.cache.Set(key, &Entry[any]{ Value: value, Timestamp: time.Now().Unix(), TTL: seconds, }) store.dirty = true } // Delete removes a key from the specified store func Delete(s Store, key string) { defer log.Trace(time.Now(), string(s), key) store := s.get() if store == nil { log.Debugf("(%s) store is nil", string(s)) return } log.Debugf("(%s) deleting key: %s", string(s), key) store.cache.Delete(key) store.dirty = true } func DeleteAll(s Store) { defer log.Trace(time.Now(), string(s)) store := s.get() if store == nil { log.Debugf("(%s) store is nil", string(s)) return } store.cache = maps.NewConcurrent[*Entry[any]]() store.dirty = true } func Print(s Store) string { defer log.Trace(time.Now(), string(s)) store := s.get() if store == nil { return fmt.Sprintf("Store %s is nil", string(s)) } cache := store.cache.ToSimple() if len(cache) == 0 { return fmt.Sprintf("Store %s is empty", string(s)) } var builder strings.Builder for key, entry := range cache { builder.WriteString("\n") if entry.Expired() { fmt.Fprintf(&builder, "Key: %s [EXPIRED]\n", key) builder.WriteString("\n") continue } var ttlInfo string if entry.TTL < 0 { ttlInfo = "never expires" } if entry.TTL >= 0 { expiresAt := time.Unix(entry.Timestamp+int64(entry.TTL), 0) ttlInfo = fmt.Sprintf("expires at %s", expiresAt.Format("2006-01-02 15:04:05")) } fmt.Fprintf(&builder, "Key: %s\n", key) fmt.Fprintf(&builder, " Value: %s\n", fmt.Sprintf("%#v", entry.Value)) fmt.Fprintf(&builder, " Type: %T\n", entry.Value) fmt.Fprintf(&builder, " Created: %s\n", time.Unix(entry.Timestamp, 0).Format("2006-01-02 15:04:05")) fmt.Fprintf(&builder, " TTL: %s\n", ttlInfo) } return builder.String() } ================================================ FILE: src/cache/store_test.go ================================================ package cache import ( "strings" "testing" "time" "github.com/stretchr/testify/assert" ) func TestStore(t *testing.T) { cases := []struct { setupFunc func() *store testFunc func(t *testing.T) name string }{ { name: "Print store with data", setupFunc: func() *store { testStore := Session.new() testStore.cache.Set("test_key1", &Entry[any]{ Value: "test_value1", Timestamp: time.Now().Unix(), TTL: 3600, // 1 hour }) testStore.cache.Set("test_key2", &Entry[any]{ Value: 42, Timestamp: time.Now().Unix(), TTL: -1, // never expires }) testStore.cache.Set("expired_key", &Entry[any]{ Value: "expired_value", Timestamp: time.Now().Unix() - 7200, // 2 hours ago TTL: 3600, // 1 hour (should be expired) }) session = testStore return testStore }, testFunc: func(t *testing.T) { result := Print(Session) assert.Contains(t, result, "Key: test_key1") assert.Contains(t, result, `Value: "test_value1"`) // Note: quotes are included in output assert.Contains(t, result, "Type: string") assert.Contains(t, result, "Key: test_key2") assert.Contains(t, result, "Value: 42") assert.Contains(t, result, "Type: int") assert.Contains(t, result, "Key: expired_key [EXPIRED]") assert.Contains(t, result, "never expires") assert.Contains(t, result, "expires at") // Verify structure lines := strings.Split(result, "\n") assert.True(t, len(lines) > 10, "Output should have multiple lines") }, }, { name: "Print empty store", setupFunc: func() *store { testStore := Session.new() session = testStore return testStore }, testFunc: func(t *testing.T) { result := Print(Session) assert.Contains(t, result, "Store session is empty") }, }, { name: "Print nil store check", setupFunc: func() *store { testStore := Session.new() session = testStore return testStore }, testFunc: func(t *testing.T) { // Since get() always creates a store, we test empty store behavior result := Print(Session) assert.Contains(t, result, "Store session is empty") }, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { tc.setupFunc() tc.testFunc(t) }) } } ================================================ FILE: src/cache/template.go ================================================ package cache import ( "github.com/jandedobbeleer/oh-my-posh/src/maps" ) type Template struct { Segments *maps.Concurrent[any] SimpleTemplate } type SimpleTemplate struct { SegmentsCache maps.Simple[any] Var maps.Simple[any] PWD string Folder string PSWD string UserName string HostName string ShellVersion string Shell string AbsolutePWD string OS string Version string PromptCount int SHLVL int Jobs int Code int WSL bool Root bool } func (t *Template) AddSegmentData(key string, value any) { t.Segments.Set(key, value) } func (t *Template) RemoveSegmentData(key string) { t.Segments.Delete(key) } ================================================ FILE: src/cli/args.go ================================================ package cli import ( "github.com/spf13/cobra" ) func NoArgsOrOneValidArg(cmd *cobra.Command, args []string) error { if len(args) == 0 { return nil } if err := cobra.ExactArgs(1)(cmd, args); err != nil { return err } return cobra.OnlyValidArgs(cmd, args) } ================================================ FILE: src/cli/auth/cli.go ================================================ package auth import ( "fmt" "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/jandedobbeleer/oh-my-posh/src/log" "github.com/jandedobbeleer/oh-my-posh/src/runtime" ) var ( program *tea.Program textStyle = lipgloss.NewStyle().Margin(1, 0, 2, 2) ) type stateMsg state type state int const ( code state = iota token done ) // ErrorGetter is implemented by auth models to get the error. type ErrorGetter interface { GetError() error } func setState(message state) { if program == nil { return } program.Send(stateMsg(message)) } type model struct { env runtime.Environment err error spinner *spinner.Model status func(error) string code string state state } func (m *model) Init() tea.Cmd { s := spinner.New() s.Spinner = spinner.Globe s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("170")) m.spinner = &s return m.spinner.Tick } func (m *model) GetError() error { return m.err } func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case stateMsg: m.state = state(msg) if m.state == done { return m, tea.Quit } return m, nil default: s, cmd := m.spinner.Update(msg) m.spinner = &s return m, cmd } } func (m *model) View() string { var message string switch m.state { case code: message = fmt.Sprintf("%s Fetching code for authentication", m.spinner.View()) case token: message = fmt.Sprintf("%s Fetching token with code: %s", m.spinner.View(), m.code) case done: message = m.status(m.err) } return textStyle.Render(message) } func Run(m tea.Model) error { program = tea.NewProgram(m) resultModel, _ := program.Run() if eg, ok := resultModel.(ErrorGetter); ok { return eg.GetError() } log.Debug("model does not implement ErrorGetter") return nil } ================================================ FILE: src/cli/auth/copilot.go ================================================ package auth import ( "encoding/json" "fmt" httplib "net/http" "strings" "time" tea "github.com/charmbracelet/bubbletea" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/runtime/http" ) const ( // GitHub Copilot's OAuth client ID - This is a public client ID used for device code flow CopilotClientID = "Iv1.b507a08c87ecfe98" CopilotScope = "read:email" CopilotDeviceCodeURL = "https://github.com/login/device/code" CopilotAccessTokenURL = "https://github.com/login/oauth/access_token" CopilotTokenKey = "copilot_token" ) // DeviceCodeResponse represents the response from GitHub's device code endpoint. type DeviceCodeResponse struct { DeviceCode string `json:"device_code"` UserCode string `json:"user_code"` VerificationURI string `json:"verification_uri"` ExpiresIn int `json:"expires_in"` Interval int `json:"interval"` } // AccessTokenResponse represents the response from GitHub's access token endpoint. type AccessTokenResponse struct { AccessToken string `json:"access_token"` TokenType string `json:"token_type"` Scope string `json:"scope"` Error string `json:"error"` ErrorDescription string `json:"error_description"` } func NewCopilot(env runtime.Environment) *CopilotAuth { return &CopilotAuth{ model: model{ env: env, }, } } type CopilotAuth struct { deviceCodeExpiry time.Time verificationURI string model lastState state } func (c *CopilotAuth) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case stateMsg: c.state = state(msg) if c.state == done { return c, tea.Quit } return c, nil default: s, cmd := c.spinner.Update(msg) c.spinner = &s return c, cmd } } func (c *CopilotAuth) Init() tea.Cmd { c.model.status = c.status cmd := c.model.Init() go c.Authenticate() return cmd } func (c *CopilotAuth) Authenticate() { setState(code) c.lastState = code deviceCode, err := c.requestDeviceCode() if err != nil { c.err = err setState(done) return } c.code = deviceCode.UserCode c.verificationURI = deviceCode.VerificationURI c.deviceCodeExpiry = time.Now().Add(time.Duration(deviceCode.ExpiresIn) * time.Second) setState(token) c.lastState = token interval := max(deviceCode.Interval, 5) token, err := c.pollForToken(deviceCode.DeviceCode, interval) if err != nil { c.err = err setState(done) return } if token == "" { c.err = fmt.Errorf("received empty token") setState(done) return } cache.Set(cache.Device, CopilotTokenKey, token, cache.TWOYEARS) setState(done) } func (c *CopilotAuth) requestDeviceCode() (*DeviceCodeResponse, error) { body := fmt.Sprintf("client_id=%s&scope=%s", CopilotClientID, CopilotScope) modifyRequest := func(request *httplib.Request) { request.Method = httplib.MethodPost request.Header.Set("Content-Type", "application/x-www-form-urlencoded") request.Header.Set("Accept", "application/json") } response, err := c.env.HTTPRequest(CopilotDeviceCodeURL, strings.NewReader(body), 30000, modifyRequest) if err != nil { return nil, fmt.Errorf("failed to request device code: %w", err) } var result DeviceCodeResponse if err := json.Unmarshal(response, &result); err != nil { return nil, fmt.Errorf("failed to parse device code response: %w", err) } return &result, nil } func (c *CopilotAuth) pollForToken(deviceCode string, interval int) (string, error) { modifyRequest := func(request *httplib.Request) { request.Method = httplib.MethodPost request.Header.Set("Content-Type", "application/x-www-form-urlencoded") request.Header.Set("Accept", "application/json") } for { if time.Now().After(c.deviceCodeExpiry) { return "", fmt.Errorf("device code expired, please try again") } time.Sleep(time.Duration(interval) * time.Second) body := fmt.Sprintf("client_id=%s&device_code=%s&grant_type=urn:ietf:params:oauth:grant-type:device_code", CopilotClientID, deviceCode) response, err := c.env.HTTPRequest(CopilotAccessTokenURL, strings.NewReader(body), 30000, modifyRequest) if err != nil { // Log error but continue polling continue } var result AccessTokenResponse if err := json.Unmarshal(response, &result); err != nil { // Log error but continue polling continue } if result.AccessToken != "" { return result.AccessToken, nil } switch result.Error { case "authorization_pending": continue case "slow_down": interval += 5 continue case "expired_token": return "", fmt.Errorf("device code expired, please try again") case "access_denied": return "", fmt.Errorf("access was denied by the user") default: if result.Error != "" { return "", fmt.Errorf("authentication error: %s - %s", result.Error, result.ErrorDescription) } } } } func (c *CopilotAuth) status(err error) string { if err == nil { return "Successfully authenticated with GitHub Copilot" } httpErr, ok := err.(*http.Error) if !ok { return err.Error() } return fmt.Sprintf("HTTP error %d: %s", httpErr.StatusCode, httpErr.Error()) } func (c *CopilotAuth) View() string { var message string switch c.state { case code: message = fmt.Sprintf("%s Requesting device code from GitHub", c.spinner.View()) case token: message = fmt.Sprintf("%s Please visit %s and enter code: %s", c.spinner.View(), c.verificationURI, c.code) case done: message = c.status(c.err) } return textStyle.Render(message) } ================================================ FILE: src/cli/auth/ytmda.go ================================================ package auth import ( "encoding/json" "errors" "fmt" "net" httplib "net/http" "strings" tea "github.com/charmbracelet/bubbletea" "github.com/jandedobbeleer/oh-my-posh/src/build" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/runtime/http" ) const ( YTMDABASEURL = "http://localhost:9863/api/v1" YTMDATOKEN = "ytmda_token" tokenURL = YTMDABASEURL + "/auth/request" codeURL = YTMDABASEURL + "/auth/requestcode" ) func NewYtmda(env runtime.Environment) *Ytmda { return &Ytmda{ model: model{ env: env, }, } } type Ytmda struct { model lastState state } func (y *Ytmda) Init() tea.Cmd { y.model.status = y.status cmd := y.model.Init() go y.Authenticate() return cmd } func (y *Ytmda) Authenticate() { setState(code) y.lastState = code code, err := y.requestCode() if err != nil { y.err = err setState(done) return } y.code = code setState(token) y.lastState = token token, err := y.requestToken(code) if err != nil { y.err = err setState(done) return } if token == "" { y.err = fmt.Errorf("received empty token") setState(done) return } cache.Set(cache.Session, YTMDATOKEN, token, cache.INFINITE) setState(done) } func (y *Ytmda) requestCode() (string, error) { body := fmt.Sprintf(`{"appId": "ohmyposh", "appName": "oh-my-posh", "appVersion": "%s"}`, strings.TrimPrefix(build.Version, "v")) type codeResponse struct { Code string `json:"code"` } result, err := ytmdaRequest[codeResponse](httplib.MethodPost, codeURL, body, y.env) return result.Code, err } func (y *Ytmda) requestToken(code string) (string, error) { body := fmt.Sprintf(`{"appId": "ohmyposh", "code": "%s"}`, code) type tokenResponse struct { Token string `json:"token"` } result, err := ytmdaRequest[tokenResponse](httplib.MethodPost, tokenURL, body, y.env) return result.Token, err } func ytmdaRequest[a any](method, url, body string, env runtime.Environment, requestModifiers ...http.RequestModifier) (a, error) { if requestModifiers == nil { requestModifiers = []http.RequestModifier{} } modifyRequest := func(request *httplib.Request) { request.Method = method request.Header.Set("Content-Type", "application/json") } requestModifiers = append(requestModifiers, modifyRequest) var result a response, err := env.HTTPRequest(url, strings.NewReader(body), 50000, requestModifiers...) if err != nil { return result, err } err = json.Unmarshal(response, &result) return result, err } func (y *Ytmda) status(err error) string { // get the status code from the error if available if err == nil { return "Successfully authenticated with YouTube Music Desktop App" } var netErr net.Error if errors.As(err, &netErr) && netErr.Timeout() { return "There was a timeout while trying to connect to the YouTube Music Desktop App Companion API. Please try again" } httpErr, ok := err.(*http.Error) if !ok { // if the error is not an http.Error, the service isn't running return "YouTube Music Desktop App is not running, please start the Companion API" } if httpErr.StatusCode != httplib.StatusForbidden { return err.Error() } if y.lastState == token { return "Failed to request token with code. Please press Allow in the pop-up window" } return "Please enable companion authorization in the YouTube Music Desktop App settings" } ================================================ FILE: src/cli/auth/ytmda_test.go ================================================ package auth import ( "errors" "testing" "github.com/jandedobbeleer/oh-my-posh/src/cache" runtime_ "github.com/jandedobbeleer/oh-my-posh/src/runtime/mock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestYtdma_Authenticate(t *testing.T) { testCases := []struct { name string requestCodeResponse string requestCodeError error requestTokenResponse string requestTokenError error expectedError error expectedToken string shouldSetToken bool }{ { name: "successful authentication", requestCodeResponse: `{"code":"test-code-123"}`, requestCodeError: nil, requestTokenResponse: `{"token":"test-token-456"}`, requestTokenError: nil, expectedError: nil, expectedToken: "test-token-456", shouldSetToken: true, }, { name: "request code fails", requestCodeResponse: "", requestCodeError: errors.New("failed to request code"), requestTokenResponse: "", requestTokenError: nil, expectedError: errors.New("failed to request code"), expectedToken: "", shouldSetToken: false, }, { name: "request token fails", requestCodeResponse: `{"code":"test-code-123"}`, requestCodeError: nil, requestTokenResponse: "", requestTokenError: errors.New("failed to request token"), expectedError: errors.New("failed to request token"), expectedToken: "", shouldSetToken: false, }, { name: "invalid code response JSON", requestCodeResponse: `{"invalid":"json"}`, requestCodeError: nil, requestTokenResponse: "", requestTokenError: nil, expectedError: errors.New("unexpected end of JSON input"), expectedToken: "", shouldSetToken: false, }, { name: "invalid token response JSON", requestCodeResponse: `{"code":"test-code-123"}`, requestCodeError: nil, requestTokenResponse: `{"invalid":"json"}`, requestTokenError: nil, expectedError: errors.New("received empty token"), expectedToken: "", shouldSetToken: false, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { env := &runtime_.Environment{} env.On("HTTPRequest", codeURL).Return([]byte(tc.requestCodeResponse), tc.requestCodeError) env.On("HTTPRequest", tokenURL).Return([]byte(tc.requestTokenResponse), tc.requestTokenError) if tc.shouldSetToken { cache.Set(cache.Device, YTMDATOKEN, tc.expectedToken, cache.INFINITE) } ytmda := &Ytmda{ model: model{ env: env, }, } ytmda.Authenticate() if tc.expectedError != nil { require.NotNil(t, ytmda.err) assert.Equal(t, tc.expectedError.Error(), ytmda.err.Error()) } else { assert.Nil(t, ytmda.err) } cache.DeleteAll(cache.Device) }) } } ================================================ FILE: src/cli/auth.go ================================================ package cli import ( "os" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/cli/auth" "github.com/jandedobbeleer/oh-my-posh/src/log" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/spf13/cobra" ) var authCmd = &cobra.Command{ Use: "auth [service]", Short: "Authenticate against a service", Long: `Authenticate against a service. Available services: - copilot: GitHub Copilot API - ytmda: YouTube Music Desktop App (YTMDA) API`, ValidArgs: []string{ "copilot", "ytmda", }, Args: NoArgsOrOneValidArg, Run: func(cmd *cobra.Command, args []string) { if len(args) == 0 { _ = cmd.Help() return } flags := &runtime.Flags{ Shell: os.Getenv("POSH_SHELL"), } env := &runtime.Terminal{} env.Init(flags) cache.Init(env.Shell(), cache.Persist) defer func() { cache.Close() }() switch args[0] { case "copilot": authenticator := auth.NewCopilot(env) if err := auth.Run(authenticator); err != nil { log.Error(err) exitcode = 70 } case "ytmda": authenticator := auth.NewYtmda(env) if err := auth.Run(authenticator); err != nil { log.Error(err) exitcode = 70 } default: _ = cmd.Help() } }, } func init() { RootCmd.AddCommand(authCmd) } ================================================ FILE: src/cli/cache.go ================================================ package cli import ( "fmt" "os" "strconv" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/spf13/cobra" ) var ( session bool ) // cacheCmd represents the cache command var cacheCmd = &cobra.Command{ Use: "cache [path|clear|ttl|show]", Short: "Interact with the oh-my-posh cache", Long: `Interact with the oh-my-posh cache. You can do the following: - path: list cache path - clear: remove all cache values - ttl: get cache TTL in days - show: print a detailed list of all cached values`, ValidArgs: []string{ "path", "clear", cache.TTL, "show", }, Args: cobra.RangeArgs(1, 2), Run: func(cmd *cobra.Command, args []string) { if len(args) == 0 { _ = cmd.Help() return } switch args[0] { case "path": fmt.Println(cache.Path()) case "clear": err := cache.Clear(true) if err != nil { fmt.Println(err) return } fmt.Println("cache cleared") case cache.TTL: // get the second argument as int if len(args) < 2 { fmt.Println("please provide a TTL value in days") exitcode = 2 return } ttl, err := strconv.Atoi(args[1]) if err != nil { fmt.Println("error parsing TTL:", err.Error()) exitcode = 2 return } cache.Init(os.Getenv("POSH_SHELL"), cache.Persist) cache.Set(cache.Device, cache.TTL, ttl, cache.INFINITE) cache.Close() case "show": cache.Init(os.Getenv("POSH_SHELL")) store := cache.Device if session { store = cache.Session } fmt.Println(cache.Print(store)) } }, } func init() { cacheCmd.Flags().BoolVarP(&session, "session", "s", false, "show the session cache") RootCmd.AddCommand(cacheCmd) } ================================================ FILE: src/cli/claude.go ================================================ package cli import ( "encoding/json" "fmt" "io" "os" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/config" "github.com/jandedobbeleer/oh-my-posh/src/log" "github.com/jandedobbeleer/oh-my-posh/src/prompt" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/segments" "github.com/jandedobbeleer/oh-my-posh/src/shell" "github.com/jandedobbeleer/oh-my-posh/src/template" "github.com/jandedobbeleer/oh-my-posh/src/terminal" "github.com/spf13/cobra" ) // claudeCmd represents the claude command var claudeCmd = &cobra.Command{ Use: "claude", Short: "Render a prompt for Claude Code statusline", Long: `Render a prompt for Claude Code statusline integration. This command reads Claude Code's contextual JSON data from stdin and renders a prompt that can include a Claude segment with session information like model name, costs, tokens, and more. Example usage in Claude Code settings: "statusLine": { "command": "oh-my-posh claude --config ~/.config/ohmyposh/claude.toml" }`, Args: cobra.NoArgs, Run: func(_ *cobra.Command, _ []string) { log.Debug("claude command started") // Read JSON from stdin stdinData, err := io.ReadAll(os.Stdin) if err != nil { log.Error(err) return } log.Debugf("received data from stdin: %s", string(stdinData)) // Process Claude data and initialize cache processClaudeData(stdinData) flags := &runtime.Flags{ ConfigPath: configFlag, Shell: shell.CLAUDE, } env := &runtime.Terminal{} env.Init(flags) var cfg *config.Config cfg, err = config.Parse(configFlag) if err != nil { cfg = config.Claude() } template.Init(env, cfg.Var, cfg.Maps) terminal.Init(shell.CLAUDE) terminal.BackgroundColor = cfg.TerminalBackground.ResolveTemplate() terminal.Colors = cfg.MakeColors(env) eng := &prompt.Engine{ Config: cfg, Env: env, } defer func() { template.SaveCache() cache.Close() }() result := eng.Status() fmt.Print(result) }, } // processClaudeData handles parsing and caching of Claude JSON data func processClaudeData(stdinData []byte) { if len(stdinData) == 0 { cache.Init(shell.CLAUDE, cache.Persist, cache.NoSession) return } var claudeData segments.ClaudeData if err := json.Unmarshal(stdinData, &claudeData); err != nil { log.Error(err) cache.Init(shell.CLAUDE, cache.Persist, cache.NoSession) return } log.Debugf("parsed Claude data: session_id=%s, model=%s", claudeData.SessionID, claudeData.Model.DisplayName) // Set the session ID from Claude data if available if claudeData.SessionID != "" { os.Setenv("POSH_SESSION_ID", claudeData.SessionID) log.Debugf("set POSH_SESSION_ID to: %s", claudeData.SessionID) } // Initialize cache first so we can store the data cache.Init(shell.CLAUDE, cache.Persist) // Store the parsed data in session cache cache.Set(cache.Session, cache.CLAUDECACHE, claudeData, cache.INFINITE) log.Debug("stored Claude data in session cache") } func init() { RootCmd.AddCommand(claudeCmd) } ================================================ FILE: src/cli/config.go ================================================ package cli import ( "fmt" "os" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/config" "github.com/jandedobbeleer/oh-my-posh/src/dsc" "github.com/spf13/cobra" ) // configCmd represents the config command var configCmd = &cobra.Command{ Use: "config edit", Short: "Interact with the config", Long: `Interact with the config. You can export, migrate or edit the config (via the editor specified in the environment variable "EDITOR").`, ValidArgs: []string{ "edit", }, Args: NoArgsOrOneValidArg, Run: func(cmd *cobra.Command, args []string) { if len(args) == 0 { _ = cmd.Help() return } switch args[0] { case "edit": cache.Init(os.Getenv("POSH_SHELL")) if configPath, OK := cache.Get[string](cache.Session, config.SourceKey); OK { exitcode = editFileWithEditor(configPath) return } fmt.Println("no config found in session cache") exitcode = 666 default: _ = cmd.Help() } }, } func init() { configCmd.AddCommand(dsc.Command(config.DSC())) RootCmd.AddCommand(configCmd) } ================================================ FILE: src/cli/config_export.go ================================================ package cli import ( "errors" "fmt" "os" "path/filepath" "strings" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/config" "github.com/jandedobbeleer/oh-my-posh/src/runtime/path" "github.com/spf13/cobra" ) var ( format string output string ) // exportCmd represents the export command var exportCmd = &cobra.Command{ Use: "export", Short: "Export your config", Long: `Export your config. You can choose to print the output to stdout, or export your config in the format of your choice. Example usage: > oh-my-posh config export --config ~/myconfig.omp.json --format toml Exports the config file "~/myconfig.omp.json" in TOML format and prints the result to stdout. > oh-my-posh config export --output ~/new_config.omp.json Exports the current config to "~/new_config.omp.json" (in JSON format).`, Args: cobra.NoArgs, Run: func(_ *cobra.Command, _ []string) { if output == "" && format == "" { // usage error fmt.Println("neither output path nor export format is specified") exitcode = 2 return } cache.Init(os.Getenv("POSH_SHELL")) err := setConfigFlag() if err != nil { exitcode = 666 fmt.Println(err.Error()) return } cfg := config.Load(configFlag) validateExportFormat := func() error { format = strings.ToLower(format) switch format { case config.JSON, config.JSONC: format = config.JSON case config.TOML, config.TML: format = config.TOML case config.YAML, config.YML: format = config.YAML default: formats := []string{config.JSON, config.JSONC, config.TOML, config.TML, config.YAML, config.YML} // usage error fmt.Printf("export format must be one of these: %s\n", strings.Join(formats, ", ")) exitcode = 2 return errors.New("invalid export format") } return nil } if len(format) != 0 { if err := validateExportFormat(); err != nil { return } } if output == "" { fmt.Print(cfg.Export(format)) return } cfg.Source = cleanOutputPath(output) if format == "" { format = strings.TrimPrefix(filepath.Ext(output), ".") if err := validateExportFormat(); err != nil { return } } cfg.Write(format) }, } func cleanOutputPath(output string) string { output = path.ReplaceTildePrefixWithHomeDir(output) if !filepath.IsAbs(output) { if absPath, err := filepath.Abs(output); err == nil { output = absPath } } return filepath.Clean(output) } func init() { exportCmd.Flags().StringVarP(&format, "format", "f", "json", "config format to migrate to") exportCmd.Flags().StringVarP(&output, "output", "o", "", "config file to export to") configCmd.AddCommand(exportCmd) } ================================================ FILE: src/cli/config_export_image.go ================================================ package cli import ( "fmt" "os" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/cli/image" "github.com/jandedobbeleer/oh-my-posh/src/config" "github.com/jandedobbeleer/oh-my-posh/src/prompt" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/shell" "github.com/jandedobbeleer/oh-my-posh/src/template" "github.com/jandedobbeleer/oh-my-posh/src/terminal" "github.com/spf13/cobra" ) var ( author string colorSettingsFile string bgColor string outputImage string ) // imageCmd represents the image command var imageCmd = &cobra.Command{ Use: "image", Short: "Export your config to an image", Long: `Export your config to an image. You can tweak the output by using additional flags: - cursor-padding: the padding of the prompt cursor - rprompt-offset: the offset of the right prompt - settings: JSON file with overrides Example usage: > oh-my-posh config export image --config ~/myconfig.omp.json Exports the config to an image file called myconfig.png in the current working directory. > oh-my-posh config export image --config ~/myconfig.omp.json --output ~/mytheme.png Exports the config to an image file ~/mytheme.png. > oh-my-posh config export image --config ~/myconfig.omp.json --settings ~/.image.settings.json Exports the config to an image file using customized output settings.`, Args: cobra.NoArgs, Run: func(_ *cobra.Command, _ []string) { cache.Init(os.Getenv("POSH_SHELL")) err := setConfigFlag() if err != nil { exitcode = 666 fmt.Println(err.Error()) return } cfg := config.Load(configFlag) flags := &runtime.Flags{ ConfigPath: cfg.Source, Shell: shell.GENERIC, TerminalWidth: 120, } env := &runtime.Terminal{} env.Init(flags) template.Init(env, cfg.Var, cfg.Maps) defer func() { template.SaveCache() cache.Close() }() // set sane defaults for things we don't print cfg.ConsoleTitleTemplate = "" cfg.PWD = "" cfg.ShellIntegration = false terminal.Init(shell.GENERIC) terminal.BackgroundColor = cfg.TerminalBackground.ResolveTemplate() terminal.Colors = cfg.MakeColors(env) eng := &prompt.Engine{ Config: cfg, Env: env, } settings, err := image.LoadSettings(colorSettingsFile) if err != nil { settings = &image.Settings{ Colors: image.NewColors(), Author: author, BackgroundColor: bgColor, } } if settings.Colors == nil { settings.Colors = image.NewColors() } if settings.Cursor == "" { settings.Cursor = "_" } primaryPrompt := eng.Primary() imageCreator := &image.Renderer{ AnsiString: primaryPrompt, Settings: *settings, } if outputImage != "" { imageCreator.Path = cleanOutputPath(outputImage) } err = imageCreator.Init(env) if err != nil { fmt.Print(err.Error()) return } err = imageCreator.SavePNG() if err != nil { fmt.Print(err.Error()) } }, } func init() { imageCmd.Flags().StringVar(&author, "author", "", "config author") imageCmd.Flags().StringVar(&bgColor, "background-color", "", "image background color") imageCmd.Flags().StringVarP(&outputImage, "output", "o", "", "image file (.png) to export to") imageCmd.Flags().StringVar(&colorSettingsFile, "settings", "", "color settings file to override ANSI color codes and metadata") // deprecated flags _ = imageCmd.Flags().MarkHidden("author") _ = imageCmd.Flags().MarkHidden("background-color") exportCmd.AddCommand(imageCmd) } func setConfigFlag() error { if configFlag != "" { return nil } configPath, OK := cache.Get[string](cache.Session, config.SourceKey) if !OK { return fmt.Errorf("no config found in session cache, please provide a config using the --config flag") } configFlag = configPath return nil } ================================================ FILE: src/cli/debug.go ================================================ package cli import ( "fmt" "os" "time" "github.com/jandedobbeleer/oh-my-posh/src/build" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/config" "github.com/jandedobbeleer/oh-my-posh/src/log" "github.com/jandedobbeleer/oh-my-posh/src/prompt" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/shell" "github.com/jandedobbeleer/oh-my-posh/src/template" "github.com/jandedobbeleer/oh-my-posh/src/terminal" "github.com/spf13/cobra" ) // debugCmd represents the debug command var ( debugCmd = createDebugCmd() startTime = time.Now() ) func init() { RootCmd.AddCommand(debugCmd) } func createDebugCmd() *cobra.Command { debugCmd := &cobra.Command{ Use: "debug", Short: "Print the prompt in debug mode", Long: "Print the prompt in debug mode.", Run: func(_ *cobra.Command, _ []string) { startTime := time.Now() log.Enable(plain) flags := &runtime.Flags{ Debug: true, PWD: pwd, Shell: shell.GENERIC, Plain: plain, } env := &runtime.Terminal{} env.Init(flags) cache.Init(os.Getenv("POSH_SHELL")) cfg := getDebugConfig(configFlag) template.Init(env, cfg.Var, cfg.Maps) defer func() { template.SaveCache() cache.Close() }() terminal.Init(shell.GENERIC) terminal.BackgroundColor = cfg.TerminalBackground.ResolveTemplate() terminal.Colors = cfg.MakeColors(env) terminal.Plain = plain eng := &prompt.Engine{ Config: cfg, Env: env, Plain: plain, } fmt.Print(eng.PrintDebug(startTime, build.Version)) }, } debugCmd.Flags().StringVar(&pwd, "pwd", "", "current working directory") // Deprecated flags, should be kept to avoid breaking CLI integration. debugCmd.Flags().StringVar(&shellName, "shell", "", "the shell to print for") // Hide flags that are deprecated or for internal use only. _ = debugCmd.Flags().MarkHidden("shell") return debugCmd } func getDebugConfig(configpath string) *config.Config { if len(configpath) != 0 { return config.Load(configpath) } reload, _ := cache.Get[bool](cache.Device, config.RELOAD) return config.Get(configpath, reload) } ================================================ FILE: src/cli/disable.go ================================================ package cli import ( "fmt" "github.com/spf13/cobra" ) // disableCmd represents the disable command var disableCmd = &cobra.Command{ Use: fmt.Sprintf(toggleUse, "disable"), Short: "Disable a feature", Long: fmt.Sprintf(toggleLong, "Disable"), ValidArgs: toggleArgs, Args: NoArgsOrOneValidArg, Run: func(cmd *cobra.Command, args []string) { if len(args) == 0 { _ = cmd.Help() return } toggleFeature(cmd, args[0], false) }, } func init() { RootCmd.AddCommand(disableCmd) } ================================================ FILE: src/cli/edit.go ================================================ package cli import ( "context" "fmt" "os" "os/exec" "strings" ) func editFileWithEditor(file string) int { editor := strings.TrimSpace(os.Getenv("EDITOR")) if editor == "" { fmt.Println(`no editor specified in the environment variable "EDITOR"`) return 1 } editor = strings.TrimSpace(editor) args := strings.Split(editor, " ") editor = args[0] args = append(args[1:], file) ctx := context.Background() cmd := exec.CommandContext(ctx, editor, args...) cmd.Stdin = os.Stdin cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { fmt.Println(err.Error()) return 1 } return 0 } ================================================ FILE: src/cli/enable.go ================================================ package cli import ( "fmt" "os" "strings" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/config" "github.com/spf13/cobra" ) var ( toggleHelpText = `%s one of the following features: ` toggleArgs = []string{ config.UPGRADENOTICE, config.AUTOUPGRADE, config.RELOAD, } toggleUse = fmt.Sprintf("%%s [%s]", strings.Join(toggleArgs, "|")) toggleLong = strings.Join(append([]string{toggleHelpText}, toggleArgs...), "\n- ") ) // enableCmd represents the enable command var enableCmd = &cobra.Command{ Use: fmt.Sprintf(toggleUse, "enable"), Short: "Enable a feature", Long: fmt.Sprintf(toggleLong, "Enable"), ValidArgs: toggleArgs, Args: NoArgsOrOneValidArg, Run: func(cmd *cobra.Command, args []string) { if len(args) == 0 { _ = cmd.Help() return } toggleFeature(cmd, args[0], true) }, } func init() { RootCmd.AddCommand(enableCmd) } func toggleFeature(cmd *cobra.Command, feature string, enable bool) { if feature == "" { _ = cmd.Help() return } cache.Init(os.Getenv("POSH_SHELL"), cache.Persist) cache.Set(cache.Device, feature, enable, cache.INFINITE) cache.Close() } ================================================ FILE: src/cli/font/download.go ================================================ package font import ( "context" "errors" "fmt" "io" httplib "net/http" "net/url" "os" "path" "path/filepath" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/cli/progress" "github.com/jandedobbeleer/oh-my-posh/src/runtime/http" ) func Download(fontURL string) ([]byte, error) { if zipPath, OK := cache.Get[string](cache.Device, 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(fontURL); err != nil { return nil, err } if !isZipFile(b) { 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(cache.Device, fontURL, zipPath, cache.ONEDAY) return b, nil } func isZipFile(data []byte) bool { contentType := httplib.DetectContentType(data) return contentType == "application/zip" } func getRemoteFile(location string) (data []byte, err error) { req, err := httplib.NewRequestWithContext(context.Background(), "GET", location, nil) 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 { return data, fmt.Errorf("failed to download zip file: %s\n→ %s", resp.Status, location) } reader := progress.NewReader(resp.Body, resp.ContentLength, program) data, err = io.ReadAll(reader) if err != nil { return } return } ================================================ FILE: src/cli/font/dsc.go ================================================ package font import ( "github.com/jandedobbeleer/oh-my-posh/src/dsc" "github.com/jandedobbeleer/oh-my-posh/src/log" ) type Resource struct { dsc.Resource[*Font] } func DSC() *Resource { return &Resource{ Resource: dsc.Resource[*Font]{}, } } func (s *Resource) Apply(schema string) error { return s.Resource.Apply(schema) } func (s *Resource) Add(name string) { if IsLocalZipFile(name) { log.Debug("Skipping local zip file font:", name) return } s.Resource.Add(&Font{ Name: name, }) } ================================================ FILE: src/cli/font/font.go ================================================ // Derived from https://github.com/Crosse/font-install // Copyright 2020 Seth Wright package font import ( "bytes" "encoding/gob" "fmt" "path" "strings" "github.com/ConradIrwin/font/sfnt" ) func init() { gob.Register([]*Font{}) gob.Register([]*Asset{}) } // Font describes a font file and the various metadata associated with it. type Font struct { Name string `json:"name,omitempty" jsonschema:"title=Font name,description=The name of the font"` Family string `json:"-"` FileName string `json:"-"` Metadata map[sfnt.NameID]string `json:"-"` Data []byte `json:"-"` } func (f *Font) Apply() error { _, err := downloadAndInstall(f.Name, "") return err } // downloadAndInstall resolves a font by name or URL, downloads it, and installs it. // It returns the resolved font name and any error encountered. func downloadAndInstall(font, zipFolder string) (string, error) { asset, err := ResolveFontAsset(font) if err != nil { return "", err } if asset.Folder != "" && zipFolder == "" { zipFolder = asset.Folder } zipFile, err := Download(asset.URL) if err != nil { return "", err } _, err = InstallZIP(zipFile, zipFolder) return asset.Name, err } func (f *Font) Equal(font *Font) bool { if font == nil { return false } return f.Name == font.Name } func (f *Font) Resolve() (*Font, bool) { return nil, false } // fontExtensions is a list of file extensions that denote fonts. // Only files ending with these extensions will be installed. var fontExtensions = map[string]bool{ ".otf": true, ".ttf": true, } // newFont creates a newFont Font struct. // fileName is the font's file name, and data is a byte slice containing the font file data. // It returns a FontData struct describing the font, or an error. func newFont(fileName string, data []byte) (*Font, error) { if _, ok := fontExtensions[strings.ToLower(path.Ext(fileName))]; !ok { return nil, fmt.Errorf("not a font: %v", fileName) } font := &Font{ FileName: fileName, Metadata: make(map[sfnt.NameID]string), Data: data, } fontData, err := sfnt.Parse(bytes.NewReader(font.Data)) if err != nil { return nil, err } if !fontData.HasTable(sfnt.TagName) { return nil, fmt.Errorf("font %v has no name table", fileName) } nameTable, err := fontData.NameTable() if err != nil { return nil, err } for _, nameEntry := range nameTable.List() { font.Metadata[nameEntry.NameID] = nameEntry.String() } font.Name = font.Metadata[sfnt.NameFull] font.Family = font.Metadata[sfnt.NamePreferredFamily] if font.Family == "" { if v, ok := font.Metadata[sfnt.NameFontFamily]; ok { font.Family = v } } if font.Name == "" { font.Name = fileName } return font, nil } ================================================ FILE: src/cli/font/fonts.go ================================================ package font import ( "context" "encoding/json" "errors" "fmt" httplib "net/http" "sort" "strings" "time" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/runtime/http" ) const ( CascadiaCodeMS = "CascadiaCode (MS)" ) type release struct { Assets []*Asset `json:"assets"` } type Asset struct { 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 } func IsLocalZipFile(name string) bool { return !strings.HasPrefix(name, "https") && strings.HasSuffix(name, ".zip") } func ResolveFontAsset(font string) (*Asset, error) { if strings.HasPrefix(font, "https") { return &Asset{URL: font}, nil } fonts, err := fonts() if err != nil { return nil, err } var asset *Asset for _, f := range fonts { if !strings.EqualFold(font, f.Name) { continue } asset = f break } if asset == nil { return nil, fmt.Errorf("no matching font found") } return asset, nil } func fonts() ([]*Asset, error) { if assets, err := getCachedFontData(); err == nil { return assets, nil } assets, err := fetchFontAssets("ryanoasis/nerd-fonts") if err != nil { return nil, err } cascadiaCode, err := CascadiaCode() if err == nil { assets = append(assets, cascadiaCode) } sort.Slice(assets, func(i, j int) bool { return assets[i].Name < assets[j].Name }) cache.Set(cache.Device, cache.FONTLISTCACHE, assets, cache.ONEDAY) return assets, nil } func getCachedFontData() ([]*Asset, error) { list, OK := cache.Get[[]*Asset](cache.Device, cache.FONTLISTCACHE) if !OK { return nil, errors.New("cache not found") } return list, nil } func CascadiaCode() (*Asset, error) { assets, err := fetchFontAssets("microsoft/cascadia-code") if err != nil || len(assets) != 1 { return nil, errors.New("no assets found") } return &Asset{ Name: CascadiaCodeMS, URL: assets[0].URL, Folder: "ttf/", }, nil } func fetchFontAssets(repo string) ([]*Asset, error) { ctx, cancelF := context.WithTimeout(context.Background(), time.Second*time.Duration(20)) defer cancelF() repoURL := "https://api.github.com/repos/" + repo + "/releases/latest" req, err := httplib.NewRequestWithContext(ctx, "GET", repoURL, nil) if err != nil { return nil, err } req.Header.Add("Accept", "application/vnd.github.v3+json") response, err := http.HTTPClient.Do(req) if err != nil || response.StatusCode != httplib.StatusOK { return nil, fmt.Errorf("failed to get %s release", repo) } defer response.Body.Close() var release release err = json.NewDecoder(response.Body).Decode(&release) if err != nil { return nil, errors.New("failed to parse nerd fonts release") } var fonts []*Asset for _, asset := range release.Assets { if asset.State == "uploaded" && strings.HasSuffix(asset.Name, ".zip") { asset.Name = strings.TrimSuffix(asset.Name, ".zip") fonts = append(fonts, asset) } } return fonts, nil } ================================================ FILE: src/cli/font/install.go ================================================ // Derived from https://github.com/Crosse/font-install // Copyright 2020 Seth Wright package font import ( "archive/zip" "bytes" "io" "path" stdruntime "runtime" "slices" "strings" "github.com/jandedobbeleer/oh-my-posh/src/log" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/runtime/cmd" ) func contains[S ~[]E, E comparable](s S, e E) bool { return slices.Contains(s, e) } func InstallZIP(data []byte, folder string) ([]string, error) { var families []string bytesReader := bytes.NewReader(data) zipReader, err := zip.NewReader(bytesReader, int64(bytesReader.Len())) if err != nil { return families, err } fonts := make(map[string]*Font) for _, file := range zipReader.File { // prevent zipslip attacks // https://security.snyk.io/research/zip-slip-vulnerability // skip folders if strings.Contains(file.Name, "..") || strings.HasSuffix(file.Name, "/") { continue } fontFileName := path.Base(file.Name) fontRelativeFileName := strings.TrimPrefix(file.Name, folder) // do not install fonts that are not in the specified installation folder if fontFileName != fontRelativeFileName { continue } fontReader, err := file.Open() if err != nil { continue } defer fontReader.Close() fontBytes, err := io.ReadAll(fontReader) if err != nil { continue } font, err := newFont(fontFileName, fontBytes) if err != nil { continue } if _, found := fonts[font.Name]; !found { fonts[font.Name] = font continue } // prefer .ttf files over other file types when we have a duplicate first := strings.ToLower(path.Ext(fonts[font.Name].FileName)) second := strings.ToLower(path.Ext(font.FileName)) if first != second && second == ".ttf" { fonts[font.Name] = font } } for _, font := range fonts { if err = install(font); err != nil { log.Error(err) continue } if found := contains(families, font.Family); !found { families = append(families, font.Family) } } // Update the font cache when installing fonts on Linux if stdruntime.GOOS == runtime.LINUX || stdruntime.GOOS == runtime.DARWIN { _, _ = cmd.Run("fc-cache", "-f") } slices.Sort(families) return families, nil } ================================================ FILE: src/cli/font/install_darwin.go ================================================ // Derived from https://github.com/Crosse/font-install // Copyright 2020 Seth Wright package font import ( "os" "path" ) var FontsDir = path.Join(os.Getenv("HOME"), "Library", "Fonts") func install(font *Font) 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. fullPath := path.Join(FontsDir, path.Base(font.FileName)) if err := os.MkdirAll(path.Dir(fullPath), 0700); err != nil { return err } return os.WriteFile(fullPath, font.Data, 0644) } ================================================ FILE: src/cli/font/install_unix.go ================================================ //go:build !windows && !darwin // Derived from https://github.com/Crosse/font-install // Copyright 2020 Seth Wright package font import ( "os" "path" "strings" ) var ( fontsDir = path.Join(os.Getenv("HOME"), "/.local/share/fonts") systemFontsDir = "/usr/share/fonts" ) func install(font *Font) error { // If we're running as root, install the font system-wide. targetDir := fontsDir if os.Geteuid() == 0 { targetDir = systemFontsDir } // 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 // of spaces). fullPath := path.Join(targetDir, strings.ToLower(strings.ReplaceAll(font.Family, " ", "-")), path.Base(font.FileName)) if err := os.MkdirAll(path.Dir(fullPath), 0700); err != nil { return err } return os.WriteFile(fullPath, font.Data, 0644) } ================================================ FILE: src/cli/font/install_windows.go ================================================ package font import ( "errors" "fmt" "os" "path/filepath" "syscall" "unsafe" "github.com/jandedobbeleer/oh-my-posh/src/log" "golang.org/x/sys/windows/registry" ) // https://docs.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-addfontresourcea const ( WM_FONTCHANGE = 0x001D HWND_BROADCAST = 0xFFFF ) func install(font *Font) error { // To install a font on Windows: // - Copy the file to the fonts directory // - Add registry entry // - Call AddFontResourceW to set the font fontsDir := filepath.Join(os.Getenv("USERPROFILE"), "AppData", "Local", "Microsoft", "Windows", "Fonts") log.Debugf("installing font %s to %s", font.FileName, fontsDir) // check if the Fonts folder exists, if not, create it if _, err := os.Stat(fontsDir); os.IsNotExist(err) { if err = os.MkdirAll(fontsDir, 0755); err != nil { return fmt.Errorf("unable to create fonts directory: %s", err.Error()) } } log.Debug("fonts directory exists, proceeding with installation") fullPath := filepath.Join(fontsDir, font.FileName) // validate if the font is already installed, remove it in case it is if _, err := os.Stat(fullPath); err == nil { log.Debugf("font %s already exists, removing it", fullPath) if err = os.Remove(fullPath); err != nil { return fmt.Errorf("unable to remove existing font file: %s", err.Error()) } } log.Debugf("writing font file to %s", fullPath) err := os.WriteFile(fullPath, font.Data, 0644) if err != nil { return fmt.Errorf("unable to write font file: %s", err.Error()) } log.Debug("font file written successfully, proceeding with registry entry") // Add registry entry reg := registry.CURRENT_USER regValue := fullPath log.Debug("opening HKEY_CURRENT_USER for writing (SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Fonts)") k, _, err := registry.CreateKey(reg, `SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts`, registry.WRITE) if err != nil { log.Error(err) // If this fails, remove the font file as well. if nexterr := os.Remove(fullPath); nexterr != nil { log.Error(nexterr) return errors.New("unable to delete font file after registry key open error") } return errors.New("unable to open HKEY_CURRENT_USER") } defer func() { err := k.Close() if err != nil { log.Error(err) } }() fontName := fmt.Sprintf("%v (TrueType)", font.Name) var alreadyInstalled, newFontType bool log.Debugf("validating if font %s is already installed", fontName) // check if we already had this key set oldFullPath, _, err := k.GetStringValue(fontName) if err == nil { log.Debugf("font %s is already installed with path %s", fontName, oldFullPath) alreadyInstalled = true newFontType = oldFullPath != fullPath } if !alreadyInstalled { log.Debug("font is not registered, adding to registry") if err := k.SetStringValue(fontName, fullPath); err != nil { return err } log.Debug("font registry entry added successfully") } // do not call AddFontResourceW if the font was already installed if alreadyInstalled && !newFontType { log.Debugf("font %s is already installed, skipping AddFontResourceW", fontName) return nil } gdi32 := syscall.NewLazyDLL("gdi32.dll") addFontResourceW := gdi32.NewProc("AddFontResourceW") // remove the old font resource in case we have a new font type with the same name if newFontType { log.Debug("removing old font resource before adding new one") fontPtr, err := syscall.UTF16PtrFromString(oldFullPath) if err == nil { removeFontResourceW := gdi32.NewProc("RemoveFontResourceW") _, _, _ = removeFontResourceW.Call(uintptr(unsafe.Pointer(fontPtr))) } } if err = k.SetStringValue(fontName, regValue); err != nil { log.Error(err) // If this fails, remove the font file as well. if nexterr := os.Remove(fullPath); nexterr != nil { return errors.New("unable to delete font file after registry key set error") } return fmt.Errorf("unable to set registry value: %s", err.Error()) } fontPtr, err := syscall.UTF16PtrFromString(fullPath) if err != nil { return err } ret, _, _ := addFontResourceW.Call(uintptr(unsafe.Pointer(fontPtr))) if ret == 0 { return errors.New("unable to add font resource using AddFontResourceW") } log.Debug("font resource added successfully") return nil } ================================================ FILE: src/cli/font/tui.go ================================================ package font import ( "fmt" "io" "os" "strings" "github.com/charmbracelet/bubbles/list" progress_ "github.com/charmbracelet/bubbles/progress" "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/jandedobbeleer/oh-my-posh/src/cli/progress" "github.com/jandedobbeleer/oh-my-posh/src/terminal" "github.com/jandedobbeleer/oh-my-posh/src/text" ) var ( program *tea.Program ) const listHeight = 14 var ( itemStyle = lipgloss.NewStyle().PaddingLeft(3) selectedItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("170")) paginationStyle = list.DefaultStyles().PaginationStyle.PaddingLeft(3) helpStyle = lipgloss.NewStyle().PaddingLeft(3).PaddingBottom(1) textStyle = lipgloss.NewStyle().Margin(1, 0, 2, 2) ) type loadMsg []*Asset type zipMsg []byte type successMsg []string type errMsg error type state int type itemDelegate struct{} func (d itemDelegate) Height() int { return 1 } func (d itemDelegate) Spacing() int { return 0 } func (d itemDelegate) Update(_ tea.Msg, _ *list.Model) tea.Cmd { return nil } func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) { //nolint: gocritic i, ok := listItem.(*Asset) if !ok { return } fn := itemStyle.Render if index == m.Index() { fn = func(s ...string) string { return selectedItemStyle.Render("•" + strings.Join(s, " ")) } } fmt.Fprint(w, fn(i.Name)) } const ( getFonts state = iota selectFont downloadFont unzipFont installFont quit done ) type main struct { err error list *list.Model spinner *spinner.Model progress *progress.Model Asset families []string state state } func (m *main) buildFontList(nerdFonts []*Asset) { var items []list.Item for _, font := range nerdFonts { items = append(items, font) } const defaultWidth = 20 l := list.New(items, itemDelegate{}, defaultWidth, listHeight) l.Title = "Select font" l.SetShowStatusBar(false) l.SetFilteringEnabled(false) l.Styles.PaginationStyle = paginationStyle l.Styles.HelpStyle = helpStyle m.list = &l } func getFontsList() { fonts, err := fonts() if err != nil { program.Send(errMsg(err)) return } program.Send(loadMsg(fonts)) } func downloadFontZip(location string) { zipFile, err := Download(location) if err != nil { program.Send(errMsg(err)) return } program.Send(zipMsg(zipFile)) } func installLocalFontZIP(m *main) { data, err := os.ReadFile(m.URL) if err != nil { program.Send(errMsg(err)) return } installFontZIP(data, m) } func installFontZIP(zipFile []byte, m *main) { families, err := InstallZIP(zipFile, m.Folder) if err != nil { program.Send(errMsg(err)) return } program.Send(successMsg(families)) } func (m *main) Init() tea.Cmd { m.progress = progress.NewModel() s := spinner.New() m.spinner = &s if len(m.URL) != 0 && !IsLocalZipFile(m.URL) { m.state = downloadFont asset, err := ResolveFontAsset(m.URL) if err != nil { m.err = err return tea.Quit } m.Asset = *asset defer func() { go downloadFontZip(asset.URL) }() m.spinner.Spinner = spinner.Globe return m.spinner.Tick } defer func() { if IsLocalZipFile(m.URL) { go installLocalFontZIP(m) return } go getFontsList() }() m.spinner.Spinner = spinner.Dot m.spinner.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("170")) m.state = getFonts if IsLocalZipFile(m.URL) { m.state = unzipFont } return m.spinner.Tick } func (m *main) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case loadMsg: m.state = selectFont m.buildFontList(msg) return m, nil case tea.WindowSizeMsg: if m.list == nil { return m, nil } m.list.SetWidth(msg.Width) return m, nil case tea.KeyMsg: switch keypress := msg.String(); keypress { case "ctrl+c", "q", "esc": m.state = quit return m, tea.Quit case "enter": 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.Asset = *font defer func() { go downloadFontZip(font.URL) }() m.spinner.Spinner = spinner.Globe return m, m.spinner.Tick case "up", "k": if m.list != nil { if m.list.Index() == 0 { m.list.Select(len(m.list.Items()) - 1) } else { m.list.Select(m.list.Index() - 1) } } return m, nil case "down", "j": if m.list != nil { if m.list.Index() == len(m.list.Items())-1 { m.list.Select(0) } else { m.list.Select(m.list.Index() + 1) } } return m, nil } case progress.Message: return m, m.progress.SetPercent(float64(msg)) case progress_.FrameMsg: return m, m.progress.Update(msg) case zipMsg: m.state = installFont defer func() { go installFontZIP(msg, m) }() m.spinner.Spinner = spinner.Dot return m, m.spinner.Tick case successMsg: m.state = done m.families = msg return m, tea.Quit case errMsg: m.err = msg return m, tea.Quit default: s, cmd := m.spinner.Update(msg) m.spinner = &s return m, cmd } if m.list == nil { return m, nil } lst, cmd := m.list.Update(msg) m.list = &lst return m, cmd } func (m *main) View() string { if m.err != nil { return textStyle.Render(m.err.Error()) } switch m.state { case getFonts: return textStyle.Render(fmt.Sprintf("%s Downloading font list%s", m.spinner.View(), terminal.StartProgress())) case selectFont: return fmt.Sprintf("\n%s%s", m.list.View(), terminal.StopProgress()) case downloadFont: return textStyle.Render(fmt.Sprintf("Downloading %s...\n%s", m.Name, m.progress.View())) case unzipFont: 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.Name)) case quit: return textStyle.Render(fmt.Sprintf("No need to install a new font? That's cool.%s", terminal.StopProgress())) case done: if len(m.families) == 0 { return textStyle.Render(fmt.Sprintf("No matching font families were installed. Try setting --zip-folder to the correct folder when using CascadiaCode (MS) or a custom font zip file. %s", terminal.StopProgress())) //nolint: lll } sb := text.NewBuilder() sb.WriteString(fmt.Sprintf("Successfully installed %s 🚀\n\n%s", m.Name, terminal.StopProgress())) sb.WriteString("The following font families are now available for configuration:\n\n") for i, family := range m.families { sb.WriteString(fmt.Sprintf(" • %s", family)) if i < len(m.families)-1 { sb.WriteString("\n") } } return textStyle.Render(sb.String()) } return "" } func Run(font, zipFolder string, headless bool) (string, error) { if headless { return installHeadless(font, zipFolder) } return tui(font, zipFolder) } func tui(font, zipFolder string) (string, error) { main := &main{ Asset: Asset{ Name: font, URL: font, Folder: zipFolder, }, } program = tea.NewProgram(main) _, err := program.Run() return main.Name, err } func installHeadless(font, zipFolder string) (string, error) { // Handle local zip file if IsLocalZipFile(font) { data, err := os.ReadFile(font) if err != nil { return "", err } _, err = InstallZIP(data, zipFolder) return font, err } return downloadAndInstall(font, zipFolder) } ================================================ FILE: src/cli/font.go ================================================ package cli import ( "fmt" "strings" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/cli/font" "github.com/jandedobbeleer/oh-my-posh/src/dsc" "github.com/jandedobbeleer/oh-my-posh/src/log" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/terminal" "github.com/spf13/cobra" ) var ( zipFolder string headless bool fontCmd = &cobra.Command{ Use: "font [install|configure]", Short: "Manage fonts", Long: `Manage fonts. This command is used to install fonts and configure the font in your terminal. - install: oh-my-posh font install 3270`, ValidArgs: []string{ "install", "configure", }, Run: func(cmd *cobra.Command, args []string) { if len(args) == 0 { _ = cmd.Help() return } switch args[0] { case "install": var fontName string if len(args) > 1 { fontName = args[1] } env := &runtime.Terminal{} env.Init(&runtime.Flags{}) sh := env.Shell() cache.Init(sh, cache.Persist) defer func() { cache.Close() }() terminal.Init(sh) if !strings.HasPrefix(zipFolder, "/") { zipFolder += "/" } fontName, err := font.Run(fontName, zipFolder, headless) if err != nil { log.Error(err) exitcode = 70 return } if env.Root() { // do not update the DSC cache if we are running as root return } fontDSC := font.DSC() fontDSC.Load() fontDSC.Add(fontName) fontDSC.Save() return case "configure": fmt.Println("not implemented") default: _ = cmd.Help() } }, } ) func init() { fontCmd.Flags().StringVar(&zipFolder, "zip-folder", "", "the folder inside the zip file to install fonts from") fontCmd.Flags().BoolVar(&headless, "headless", false, "install font without TUI") fontCmd.AddCommand(dsc.Command(font.DSC())) RootCmd.AddCommand(fontCmd) } ================================================ FILE: src/cli/get.go ================================================ package cli import ( "fmt" "os" "time" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/color" "github.com/jandedobbeleer/oh-my-posh/src/runtime" color2 "github.com/gookit/color" "github.com/spf13/cobra" ) // getCmd represents the get command var getCmd = &cobra.Command{ Use: "get [shell|millis|accent|toggles|width]", Short: "Get a value from oh-my-posh", Long: `Get a value from oh-my-posh. This command is used to get the value of the following variables: - shell - millis - accent - toggles - width`, ValidArgs: []string{ "millis", "shell", "accent", "toggles", "width", cache.TTL, }, Args: NoArgsOrOneValidArg, Run: func(cmd *cobra.Command, args []string) { if len(args) == 0 { _ = cmd.Help() return } if args[0] == "millis" { fmt.Print(time.Now().UnixNano() / 1000000) return } flags := &runtime.Flags{ Shell: os.Getenv("POSH_SHELL"), } env := &runtime.Terminal{} env.Init(flags) switch args[0] { case "shell": fmt.Print(env.Shell()) return case "accent": rgb, err := color.GetAccentColor(env) if err != nil { fmt.Println("error getting accent color:", err.Error()) return } accent := color2.RGB(rgb.R, rgb.G, rgb.B) fmt.Print("#" + accent.Hex()) return case "width": width, err := env.TerminalWidth() if err != nil { fmt.Println("error getting terminal width:", err.Error()) return } fmt.Print(width) return } cache.Init(env.Shell(), cache.Persist) defer func() { cache.Close() }() switch args[0] { case "toggles": togglesMap, _ := cache.Get[map[string]bool](cache.Session, cache.TOGGLECACHE) if len(togglesMap) == 0 { fmt.Println("No segments are toggled off") return } fmt.Println("Toggled off segments:") for toggle := range togglesMap { fmt.Println("- " + toggle) } case cache.TTL: fmt.Print(cache.GetTTL()) default: _ = cmd.Help() } }, } func init() { RootCmd.AddCommand(getCmd) } ================================================ FILE: src/cli/image/config.go ================================================ package image import ( "encoding/json" "fmt" "os" "strconv" "strings" ) // Settings represents the structure for base 16 color overrides and other image settings. // Expected JSON format: // // { // "colors": { // "red": "#FF0000", // "blue": "#0000FF", // "green": "#00FF00" // }, // "author": "Your Name", // "background_color": "#FFFFFF" // } type Settings struct { Colors Colors `json:"colors"` Author string `json:"author"` BackgroundColor string `json:"background_color"` Fonts *Fonts `json:"fonts"` Cursor string `json:"cursor,omitempty"` } type Colors map[string]HexColor func NewColors() Colors { return map[string]HexColor{} } func LoadSettings(filePath string) (*Settings, error) { if filePath == "" { return nil, fmt.Errorf("color settings file path is empty") } data, err := os.ReadFile(filePath) if err != nil { return nil, fmt.Errorf("failed to read color settings file: %w", err) } var settings Settings if err := json.Unmarshal(data, &settings); err != nil { return nil, fmt.Errorf("failed to parse color settings: %w", err) } return &settings, nil } type HexColor string func (color HexColor) RGB() (*RGB, error) { hex := string(color) hex = strings.TrimPrefix(hex, "#") if len(hex) != 6 { return nil, fmt.Errorf("invalid hex color format: %s", hex) } var r, g, b int64 var err error if r, err = strconv.ParseInt(hex[0:2], 16, 64); err != nil { return nil, err } if g, err = strconv.ParseInt(hex[2:4], 16, 64); err != nil { return nil, err } if b, err = strconv.ParseInt(hex[4:6], 16, 64); err != nil { return nil, err } return &RGB{int(r), int(g), int(b)}, nil } func (colors Colors) RGBFromColorName(colorName string) (*RGB, error) { if colors == nil || colorName == "" { return nil, fmt.Errorf("colors map or colorName is empty") } if hexColor, exists := colors[colorName]; exists { return hexColor.RGB() } return nil, fmt.Errorf("color name '%s' not found in colors map", colorName) } ================================================ FILE: src/cli/image/config_test.go ================================================ package image import ( "os" "path/filepath" "testing" "github.com/stretchr/testify/assert" ) func TestLoadSettings(t *testing.T) { cases := []struct { expectedResult *Settings name string jsonContent string expectError bool }{ { name: "Valid settings with all fields", jsonContent: `{ "colors": { "red": "#FF0000", "blue": "#0000FF", "green": "#00FF00" }, "author": "John Doe", "background_color": "#FFFFFF" }`, expectedResult: &Settings{ Colors: map[string]HexColor{ "red": "#FF0000", "blue": "#0000FF", "green": "#00FF00", }, Author: "John Doe", BackgroundColor: "#FFFFFF", }, expectError: false, }, { name: "Valid settings with only colors", jsonContent: `{ "colors": { "red": "#FF6B6B", "yellow": "#FFA07A" } }`, expectedResult: &Settings{ Colors: map[string]HexColor{ "red": "#FF6B6B", "yellow": "#FFA07A", }, Author: "", BackgroundColor: "", }, expectError: false, }, { name: "Valid settings with only author", jsonContent: `{ "author": "Jane Smith" }`, expectedResult: &Settings{ Colors: nil, Author: "Jane Smith", BackgroundColor: "", }, expectError: false, }, { name: "Empty JSON object", jsonContent: `{}`, expectedResult: &Settings{ Colors: nil, Author: "", BackgroundColor: "", }, expectError: false, }, { name: "Invalid JSON", jsonContent: `{ "colors": { "red": "#FF0000" "author": "John Doe" }`, expectedResult: nil, expectError: true, }, { name: "JSON with invalid color format", jsonContent: `{ "colors": { "red": "not-a-color" } }`, expectedResult: &Settings{ Colors: map[string]HexColor{ "red": "not-a-color", }, Author: "", BackgroundColor: "", }, expectError: false, }, { name: "JSON with extended color names", jsonContent: `{ "colors": { "lightRed": "#FF9999", "darkGray": "#333333", "lightBlue": "#87CEEB" } }`, expectedResult: &Settings{ Colors: map[string]HexColor{ "lightRed": "#FF9999", "darkGray": "#333333", "lightBlue": "#87CEEB", }, Author: "", BackgroundColor: "", }, expectError: false, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { // Create a temporary file tempFile := createTempFile(t, tc.jsonContent) defer os.Remove(tempFile) // Test LoadSettings result, err := LoadSettings(tempFile) if tc.expectError { assert.Error(t, err) assert.Nil(t, result) } else { assert.NoError(t, err) assert.Equal(t, tc.expectedResult, result) } }) } } // Helper interface for testing types that have TempDir method type testingInterface interface { TempDir() string Helper() } // Helper function to create a temporary file with given content func createTempFile(t testingInterface, content string) string { t.Helper() tempDir := t.TempDir() tempFile := filepath.Join(tempDir, "test-settings.json") err := os.WriteFile(tempFile, []byte(content), 0644) if err != nil { panic(err) // Use panic since we can't return error from generic interface } return tempFile } ================================================ FILE: src/cli/image/fonts.go ================================================ package image import ( "fmt" stdOS "os" "time" "github.com/jandedobbeleer/oh-my-posh/src/log" "golang.org/x/image/font" "golang.org/x/image/font/opentype" ) const ( regular = "regular" ) type Fonts struct { Regular string `json:"regular"` Bold string `json:"bold"` Italic string `json:"italic"` } func (f *Fonts) IsValid() bool { if f == nil { return false } // Check that all required font paths are non-empty return f.Regular != "" && f.Bold != "" && f.Italic != "" } func (f *Fonts) Load() (map[string]font.Face, error) { defer log.Trace(time.Now()) result := make(map[string]font.Face) fonts := map[string]Font{ regular: Font(f.Regular), bold: Font(f.Bold), italic: Font(f.Italic), } for name, fontPath := range fonts { fontFace, err := fontPath.Load() if err != nil { return nil, fmt.Errorf("failed to load font %s: %w", fontPath, err) } result[name] = fontFace } return result, nil } type Font string func (f Font) Load() (font.Face, error) { defer log.Trace(time.Now(), string(f)) data, err := stdOS.ReadFile(string(f)) if err != nil { return nil, fmt.Errorf("failed to read font file %s: %w", f, err) } fontObject, err := opentype.Parse(data) // handle collections if err != nil { collection, err := opentype.ParseCollection(data) if err != nil { return nil, fmt.Errorf("failed to parse font %s as single font or collection: %w", f, err) } if collection.NumFonts() == 0 { return nil, fmt.Errorf("font collection %s is empty", f) } fontObject, err = collection.Font(0) if err != nil { return nil, fmt.Errorf("failed to get first font from collection %s: %w", f, err) } } face, err := opentype.NewFace(fontObject, &opentype.FaceOptions{Size: 2.0 * 12, DPI: 144}) if err != nil { return nil, fmt.Errorf("failed to create font face for %s: %w", f, err) } if face == nil { return nil, fmt.Errorf("failed to create font face for %s: face is nil", f) } return face, nil } ================================================ FILE: src/cli/image/image.go ================================================ // Copyright © 2020 The Homeport Team // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // https://github.com/homeport/termshot package image import ( "archive/zip" "bytes" "fmt" "image" "io" "math" stdOS "os" "path/filepath" "slices" "strconv" "strings" "github.com/jandedobbeleer/oh-my-posh/src/cache" font_ "github.com/jandedobbeleer/oh-my-posh/src/cli/font" "github.com/jandedobbeleer/oh-my-posh/src/regex" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/esimov/stackblur-go" "github.com/fogleman/gg" "golang.org/x/image/font" "golang.org/x/image/font/opentype" ) type ConnectionError struct { reason string } func (f *ConnectionError) Error() string { return f.reason } const ( red = "#ED655A" yellow = "#E1C04C" green = "#71BD47" // known ansi sequences fg = "FG" bg = "BG" bc = "BC" // for base 16 colors str = "STR" text = "TEXT" invertedColor = "inverted" invertedColorSingle = "invertedsingle" fullColor = "full" foreground = "foreground" background = "background" reset = "reset" bold = "bold" boldReset = "boldr" italic = "italic" italicReset = "italicr" underline = "underline" underlineReset = "underliner" overline = "overline" overlineReset = "overliner" strikethrough = "strikethrough" strikethroughReset = "strikethroughr" backgroundReset = "backgroundr" color16 = "color16" left = "left" lineChange = "linechange" consoleTitle = "title" link = "link" ) type RGB struct { r int g int b int } func NewRGBColor(ansiColor string) *RGB { colors := strings.Split(ansiColor, ";") b, _ := strconv.Atoi(colors[2]) g, _ := strconv.Atoi(colors[1]) r, _ := strconv.Atoi(colors[0]) return &RGB{ r: r, g: g, b: b, } } type Renderer struct { italic font.Face bold font.Face regular font.Face backgroundColor *RGB ansiSequenceRegexMap map[string]string foregroundColor *RGB defaultBackgroundColor *RGB defaultForegroundColor *RGB Settings Path string AnsiString string shadowBaseColor string style string shadowOffsetX float64 margin float64 factor float64 shadowOffsetY float64 rows int lineSpacing float64 columns int padding float64 shadowRadius uint8 } func (ir *Renderer) Init(env runtime.Environment) error { ir.setOutputPath(env.Flags().ConfigPath) ir.cleanContent() if err := ir.loadFonts(); err != nil { return err } ir.initDefaults() return nil } func (ir *Renderer) loadFonts() error { if !ir.Fonts.IsValid() { return ir.loadDefaultFonts() } fonts, err := ir.Fonts.Load() if err != nil { return err } ir.regular = fonts[regular] ir.bold = fonts[bold] ir.italic = fonts[italic] return nil } func (ir *Renderer) initDefaults() { ir.defaultForegroundColor = &RGB{255, 255, 255} ir.defaultBackgroundColor = &RGB{21, 21, 21} ir.factor = 2.0 ir.columns = 80 ir.rows = 25 ir.margin = ir.factor * 48 ir.padding = ir.factor * 24 ir.shadowBaseColor = "#10101066" ir.shadowRadius = uint8(math.Min(ir.factor*16, 255)) ir.shadowOffsetX = ir.factor * 16 ir.shadowOffsetY = ir.factor * 16 ir.lineSpacing = 1.2 // Set background color from settings if provided, otherwise use default if ir.BackgroundColor == "" { ir.BackgroundColor = "#151515" // Default dark background } ir.ansiSequenceRegexMap = map[string]string{ invertedColor: `^(?P(\x1b\[38;2;(?P(\d+;?){3});49m){1}(\x1b\[7m))`, invertedColorSingle: `^(?P\x1b\[(?P\d{2,3});49m\x1b\[7m)`, fullColor: `^(?P(\x1b\[48;2;(?P(\d+;?){3})m)(\x1b\[38;2;(?P(\d+;?){3})m))`, foreground: `^(?P(\x1b\[38;2;(?P(\d+;?){3})m))`, background: `^(?P(\x1b\[48;2;(?P(\d+;?){3})m))`, reset: `^(?P\x1b\[0m)`, bold: `^(?P\x1b\[1m)`, boldReset: `^(?P\x1b\[22m)`, italic: `^(?P\x1b\[3m)`, italicReset: `^(?P\x1b\[23m)`, underline: `^(?P\x1b\[4m)`, underlineReset: `^(?P\x1b\[24m)`, overline: `^(?P\x1b\[53m)`, overlineReset: `^(?P\x1b\[55m)`, strikethrough: `^(?P\x1b\[9m)`, strikethroughReset: `^(?P\x1b\[29m)`, backgroundReset: `^(?P\x1b\[49m)`, color16: `^(?P\x1b\[(?P[349][0-7]|10[0-7]|39)m)`, left: `^(?P\x1b\[(\d{1,3})D)`, lineChange: `^(?P\x1b\[(\d)[FB])`, consoleTitle: `^(?P\x1b\]0;(.+)\007)`, link: fmt.Sprintf(`^%s`, regex.LINK), } } func (ir *Renderer) setOutputPath(config string) { if len(ir.Path) != 0 { return } if config == "" { ir.Path = "prompt.png" return } config = filepath.Base(config) match := regex.FindNamedRegexMatch(`(\.?)(?P.*)\.(json|yaml|yml|toml|jsonc)`, config) path := strings.TrimRight(match[str], ".omp") if path == "" { path = "prompt" } ir.Path = fmt.Sprintf("%s.png", path) } func (ir *Renderer) loadDefaultFonts() error { var data []byte fontCachePath := filepath.Join(cache.Path(), "Hack.zip") if _, err := stdOS.Stat(fontCachePath); err == nil { data, _ = stdOS.ReadFile(fontCachePath) } // Download font if not cached if data == nil { url := "https://github.com/ryanoasis/nerd-fonts/releases/download/v3.2.1/Hack.zip" var err error data, err = font_.Download(url) if err != nil { return &ConnectionError{reason: err.Error()} } err = stdOS.WriteFile(fontCachePath, data, 0644) if err != nil { return err } } bytesReader := bytes.NewReader(data) zipReader, err := zip.NewReader(bytesReader, int64(bytesReader.Len())) if err != nil { return err } fontFaceOptions := &opentype.FaceOptions{Size: 2.0 * 12, DPI: 144} parseFont := func(file *zip.File) (font.Face, error) { rc, err := file.Open() if err != nil { return nil, err } defer rc.Close() data, err := io.ReadAll(rc) if err != nil { return nil, err } font, err := opentype.Parse(data) if err != nil { return nil, err } fontFace, err := opentype.NewFace(font, fontFaceOptions) if err != nil { return nil, err } return fontFace, nil } for _, file := range zipReader.File { switch file.Name { case "HackNerdFont-Regular.ttf": if regular, err := parseFont(file); err == nil { ir.regular = regular } case "HackNerdFont-Bold.ttf": if bold, err := parseFont(file); err == nil { ir.bold = bold } case "HackNerdFont-Italic.ttf": if italic, err := parseFont(file); err == nil { ir.italic = italic } } } return nil } func (ir *Renderer) fontHeight() float64 { return float64(ir.regular.Metrics().Height >> 6) } type RuneRange struct { Start rune End rune } // If we're a Nerd Font code point, treat as double width var doubleWidthRunes = []RuneRange{ // Seti-UI + Custom range {Start: '\ue5fa', End: '\ue6b1'}, // Devicons {Start: '\ue700', End: '\ue7c5'}, // Font Awesome {Start: '\uf000', End: '\uf2e0'}, // Font Awesome Extension {Start: '\ue200', End: '\ue2a9'}, // Material Design Icons {Start: '\U000f0001', End: '\U000f1af0'}, // Weather {Start: '\ue300', End: '\ue3e3'}, // Octicons {Start: '\uf400', End: '\uf532'}, {Start: '\u2665', End: '\u2665'}, {Start: '\u26A1', End: '\u26A1'}, // Powerline Extra Symbols (intentionally excluding single width bubbles (e0b4-e0b7) and pixelated (e0c4-e0c7)) {Start: '\ue0a3', End: '\ue0a3'}, {Start: '\ue0b4', End: '\ue0c8'}, {Start: '\ue0ca', End: '\ue0ca'}, {Start: '\ue0cc', End: '\ue0d4'}, // IEC Power Symbols {Start: '\u23fb', End: '\u23fe'}, {Start: '\u2b58', End: '\u2b58'}, // Font Logos {Start: '\uf300', End: '\uf372'}, // Pomicons {Start: '\ue000', End: '\ue00a'}, // Codicons {Start: '\uea60', End: '\uebeb'}, } // This is getting how many additional characters of width to allocate when drawing // e.g. for characters that are 2 or more wide. A standard character will return 0 // Nerd Font glyphs will return 1, since most are double width func (ir *Renderer) runeAdditionalWidth(r rune) int { // exclude the round leading diamond singles := []rune{'\ue0b6', '\ue0ba', '\ue0bc'} if slices.Contains(singles, r) { return 0 } for _, runeRange := range doubleWidthRunes { if runeRange.Start <= r && r <= runeRange.End { return 1 } } return 0 } func (ir *Renderer) 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.Cursor } ir.AnsiString = strings.ReplaceAll(ir.AnsiString, saveCursorAnsi, ir.Cursor) // 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 *Renderer) measureContent() (width, height float64) { // Use actual rendering logic for accurate width measurement // This simulates the exact same process as the actual drawing to ensure // the canvas width perfectly matches the rendered content width var maxX float64 var x float64 // Save original ansi string and style state originalAnsi := ir.AnsiString originalStyle := ir.style ir.style = "" tmpDrawer := &font.Drawer{Face: ir.regular} for ir.AnsiString != "" { if !ir.processAnsiSequence() { continue } runes := []rune(ir.AnsiString) if len(runes) == 0 { continue } str := string(runes[0:1]) ir.AnsiString = string(runes[1:]) // Use appropriate font face for measurement var face font.Face switch ir.style { case bold: face = ir.bold case italic: face = ir.italic default: face = ir.regular } tmpDrawer.Face = face advance := tmpDrawer.MeasureString(str) w := float64(advance >> 6) // Add additional width for Nerd Font glyphs w += (w * float64(ir.runeAdditionalWidth(runes[0]))) if str == "\n" { x = 0 continue } x += w if x > maxX { maxX = x } } // Restore original state ir.AnsiString = originalAnsi ir.style = originalStyle // Ensure we have a minimum width for very short content minWidth := tmpDrawer.MeasureString(strings.Repeat(" ", 80)) width = math.Max(maxX, float64(minWidth>>6)) // height, lines times font height and line spacing lines := strings.Split(originalAnsi, "\n") height = float64(len(lines)) * ir.fontHeight() * ir.lineSpacing return width, height } func (ir *Renderer) SavePNG() error { var scale = func(value float64) float64 { return ir.factor * value } var ( corner = scale(6) radius = scale(9) distance = scale(25) ) contentWidth, contentHeight := ir.measureContent() // Make sure the output window is big enough in case no content or very few // content will be rendered. Also account for potential font variations. minRequiredWidth := 3*distance + 3*radius // Add extra buffer for wider fonts (20% more than minimum) minRequiredWidth *= 1.2 contentWidth = math.Max(contentWidth, minRequiredWidth) marginX, marginY := ir.margin, ir.margin paddingX, paddingY := ir.padding, ir.padding xOffset := marginX yOffset := marginY titleOffset := scale(40) width := contentWidth + 2*marginX + 2*paddingX height := contentHeight + 2*marginY + 2*paddingY + titleOffset dc := gg.NewContext(int(width), int(height)) xOffset -= ir.shadowOffsetX / 2 yOffset -= ir.shadowOffsetY / 2 bc := gg.NewContext(int(width), int(height)) bc.DrawRoundedRectangle(xOffset+ir.shadowOffsetX, yOffset+ir.shadowOffsetY, width-2*marginX, height-2*marginY, corner) bc.SetHexColor(ir.shadowBaseColor) bc.Fill() dst := image.NewNRGBA(bc.Image().Bounds()) // var done = make(chan struct{}, ir.shadowRadius) err := stackblur.Process( dst, bc.Image(), uint32(ir.shadowRadius), ) if err != nil { return err } // <-done dc.DrawImage(dst, 0, 0) // Draw rounded rectangle with outline and three button to produce the // impression of a window with controls and a content area dc.DrawRoundedRectangle(xOffset, yOffset, width-2*marginX, height-2*marginY, corner) dc.SetHexColor(ir.BackgroundColor) dc.Fill() dc.DrawRoundedRectangle(xOffset, yOffset, width-2*marginX, height-2*marginY, corner) dc.SetHexColor("#404040") dc.SetLineWidth(scale(1)) dc.Stroke() for i, color := range []string{red, yellow, green} { dc.DrawCircle(xOffset+paddingX+float64(i)*distance+scale(4), yOffset+paddingY+scale(4), radius) dc.SetHexColor(color) dc.Fill() } // Apply the actual text into the prepared content area of the window var x, y = xOffset + paddingX, yOffset + paddingY + titleOffset + ir.fontHeight() for ir.AnsiString != "" { if !ir.processAnsiSequence() { continue } runes := []rune(ir.AnsiString) if len(runes) == 0 { continue } str := string(runes[0:1]) ir.AnsiString = string(runes[1:]) switch ir.style { case bold: dc.SetFontFace(ir.bold) case italic: dc.SetFontFace(ir.italic) default: dc.SetFontFace(ir.regular) } w, _ := dc.MeasureString(str) // The gg library unfortunately returns a single character width for *all* glyphs in a font. // So if we know the glyph to occupy n additional characters in width, allocate that area // e.g. this will double the space for Nerd Fonts, but some could even be 3 or 4 wide // If there's 0 additional characters of width (the common case), this won't add anything w += (w * float64(ir.runeAdditionalWidth(runes[0]))) if ir.backgroundColor != nil { dc.SetRGB255(ir.backgroundColor.r, ir.backgroundColor.g, ir.backgroundColor.b) // Use consistent line height for all background rectangles fontLineHeight := ir.fontHeight() * ir.lineSpacing // Center all characters (including powerline glyphs) within the line height // Position background to align properly with text baseline and ensure consistent height bgY := y - fontLineHeight*0.75 // Adjusted for better centering with text bgHeight := fontLineHeight dc.DrawRectangle(x, bgY, w, bgHeight) dc.Fill() } if ir.foregroundColor != nil { dc.SetRGB255(ir.foregroundColor.r, ir.foregroundColor.g, ir.foregroundColor.b) } else { dc.SetRGB255(ir.defaultForegroundColor.r, ir.defaultForegroundColor.g, ir.defaultForegroundColor.b) } if str == "\n" { x = xOffset + paddingX y += ir.fontHeight() * ir.lineSpacing // Use consistent line height instead of character height continue } dc.DrawString(str, x, y) if ir.style == underline { dc.DrawLine(x, y+scale(4), x+w, y+scale(4)) dc.SetLineWidth(scale(1)) dc.Stroke() } if ir.style == overline { dc.DrawLine(x, y-scale(22), x+w, y-scale(22)) dc.SetLineWidth(scale(1)) dc.Stroke() } x += w } return dc.SavePNG(ir.Path) } func (ir *Renderer) processAnsiSequence() bool { for sequence, re := range ir.ansiSequenceRegexMap { match := regex.FindNamedRegexMatch(re, ir.AnsiString) if len(match) == 0 { continue } ir.AnsiString = strings.TrimPrefix(ir.AnsiString, match[str]) switch sequence { case invertedColor: ir.foregroundColor = ir.defaultBackgroundColor ir.backgroundColor = NewRGBColor(match[bg]) return false case invertedColorSingle: ir.foregroundColor = ir.defaultBackgroundColor bgColor, _ := strconv.Atoi(match[bg]) bgColor += 10 ir.setBase16Color(fmt.Sprint(bgColor)) return false case fullColor: ir.foregroundColor = NewRGBColor(match[fg]) ir.backgroundColor = NewRGBColor(match[bg]) return false case foreground: ir.foregroundColor = NewRGBColor(match[fg]) return false case background: ir.backgroundColor = NewRGBColor(match[bg]) return false case reset: ir.foregroundColor = ir.defaultForegroundColor ir.backgroundColor = nil return false case backgroundReset: ir.backgroundColor = nil return false case bold, italic, underline, overline: ir.style = sequence return false case boldReset, italicReset, underlineReset, overlineReset: ir.style = "" return false case strikethrough, strikethroughReset, left, lineChange, consoleTitle: return false case color16: ir.setBase16Color(match[bc]) return false case link: ir.AnsiString = match[text] + ir.AnsiString } } return true } func (ir *Renderer) setBase16Color(colorStr string) { tempColor := ir.defaultForegroundColor colorInt, err := strconv.Atoi(colorStr) if err != nil { ir.foregroundColor = tempColor return } // Check for color override first colorName := colorNameFromCode(colorInt) if rgb, err := ir.Colors.RGBFromColorName(colorName); err == nil { tempColor = rgb } // If no override found, use default colors if tempColor == ir.defaultForegroundColor { switch colorInt { case 30, 40: // Black tempColor = &RGB{1, 1, 1} case 31, 41: // Red tempColor = &RGB{222, 56, 43} case 32, 42: // Green tempColor = &RGB{57, 181, 74} case 33, 43: // Yellow tempColor = &RGB{255, 199, 6} case 34, 44: // Blue tempColor = &RGB{0, 111, 184} case 35, 45: // Magenta tempColor = &RGB{118, 38, 113} case 36, 46: // Cyan tempColor = &RGB{44, 181, 233} case 37, 47: // White tempColor = &RGB{204, 204, 204} case 90, 100: // Bright Black (Gray) tempColor = &RGB{128, 128, 128} case 91, 101: // Bright Red tempColor = &RGB{255, 0, 0} case 92, 102: // Bright Green tempColor = &RGB{0, 255, 0} case 93, 103: // Bright Yellow tempColor = &RGB{255, 255, 0} case 94, 104: // Bright Blue tempColor = &RGB{0, 0, 255} case 95, 105: // Bright Magenta tempColor = &RGB{255, 0, 255} case 96, 106: // Bright Cyan tempColor = &RGB{101, 194, 205} case 97, 107: // Bright White tempColor = &RGB{255, 255, 255} } } if colorInt < 40 || (colorInt >= 90 && colorInt < 100) { ir.foregroundColor = tempColor return } ir.backgroundColor = tempColor } // colorNameFromCode maps ANSI color codes to color names func colorNameFromCode(colorInt int) string { switch colorInt { case 30, 40: return "black" case 31, 41: return "red" case 32, 42: return "green" case 33, 43: return "yellow" case 34, 44: return "blue" case 35, 45: return "magenta" case 36, 46: return "cyan" case 37, 47: return "white" case 90, 100: return "darkGray" case 91, 101: return "lightRed" case 92, 102: return "lightGreen" case 93, 103: return "lightYellow" case 94, 104: return "lightBlue" case 95, 105: return "lightMagenta" case 96, 106: return "lightCyan" case 97, 107: return "lightWhite" default: return "" } } ================================================ FILE: src/cli/image/image_test.go ================================================ package image import ( "testing" "github.com/stretchr/testify/assert" ) func TestSetOutputPath(t *testing.T) { cases := []struct { Case string Config string Path string Expected string }{ {Case: "default config", Expected: "prompt.png"}, {Case: "hidden file", Config: ".posh.omp.json", Expected: "posh.png"}, {Case: "hidden file toml", Config: ".posh.omp.toml", Expected: "posh.png"}, {Case: "hidden file yaml", Config: ".posh.omp.yaml", Expected: "posh.png"}, {Case: "hidden file yml", Config: ".posh.omp.yml", Expected: "posh.png"}, {Case: "path provided", Path: "mytheme.png", Expected: "mytheme.png"}, {Case: "relative, no omp", Config: "~/jandedobbeleer.json", Expected: "jandedobbeleer.png"}, {Case: "relative path", Config: "~/jandedobbeleer.omp.json", Expected: "jandedobbeleer.png"}, {Case: "invalid config name", Config: "~/jandedobbeleer.omp.foo", Expected: "prompt.png"}, } for _, tc := range cases { image := &Renderer{ Path: tc.Path, } image.setOutputPath(tc.Config) assert.Equal(t, tc.Expected, image.Path, tc.Case) } } func TestHexToRGB(t *testing.T) { cases := []struct { expected *RGB name string hex HexColor hasError bool }{ { name: "Valid hex with hash", hex: "#FF0000", expected: &RGB{255, 0, 0}, hasError: false, }, { name: "Valid hex without hash", hex: "00FF00", expected: &RGB{0, 255, 0}, hasError: false, }, { name: "Valid hex blue", hex: "#0000FF", expected: &RGB{0, 0, 255}, hasError: false, }, { name: "Invalid hex too short", hex: "#FFF", expected: nil, hasError: true, }, { name: "Invalid hex too long", hex: "#FFFFFFF", expected: nil, hasError: true, }, { name: "Invalid hex characters", hex: "#GGGGGG", expected: nil, hasError: true, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { result, err := tc.hex.RGB() if tc.hasError { assert.Error(t, err) assert.Nil(t, result) } else { assert.NoError(t, err) assert.Equal(t, tc.expected, result) } }) } } func TestGetColorNameFromCode(t *testing.T) { cases := []struct { expected string colorCode int }{ {"black", 30}, {"red", 31}, {"green", 32}, {"yellow", 33}, {"blue", 34}, {"magenta", 35}, {"cyan", 36}, {"white", 37}, {"black", 40}, // background {"red", 41}, // background {"darkGray", 90}, {"lightRed", 91}, {"lightGreen", 92}, {"lightYellow", 93}, {"lightBlue", 94}, {"lightMagenta", 95}, {"lightCyan", 96}, {"lightWhite", 97}, {"", 999}, // invalid code } for _, tc := range cases { t.Run(tc.expected, func(t *testing.T) { result := colorNameFromCode(tc.colorCode) assert.Equal(t, tc.expected, result) }) } } func TestSetBase16Color(t *testing.T) { cases := []struct { colorOverrides map[string]HexColor expectedForeground *RGB expectedBackground *RGB name string colorCode string }{ { name: "Red foreground with override", colorCode: "31", colorOverrides: map[string]HexColor{"red": "#FF6B6B", "blue": "#4ECDC4"}, expectedForeground: &RGB{255, 107, 107}, expectedBackground: nil, }, { name: "Blue background with override", colorCode: "44", colorOverrides: map[string]HexColor{"red": "#FF6B6B", "blue": "#4ECDC4"}, expectedForeground: nil, expectedBackground: &RGB{78, 205, 196}, }, { name: "Green foreground without override", colorCode: "32", colorOverrides: map[string]HexColor{"red": "#FF6B6B", "blue": "#4ECDC4"}, expectedForeground: &RGB{57, 181, 74}, expectedBackground: nil, }, { name: "Red foreground without any overrides", colorCode: "31", colorOverrides: nil, expectedForeground: &RGB{222, 56, 43}, expectedBackground: nil, }, { name: "Blue background without any overrides", colorCode: "44", colorOverrides: nil, expectedForeground: nil, expectedBackground: &RGB{0, 111, 184}, }, { name: "Invalid color code", colorCode: "invalid", colorOverrides: nil, expectedForeground: &RGB{255, 255, 255}, expectedBackground: nil, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { renderer := &Renderer{ defaultForegroundColor: &RGB{255, 255, 255}, Settings: Settings{ Colors: tc.colorOverrides, }, } renderer.setBase16Color(tc.colorCode) if tc.expectedForeground != nil { assert.Equal(t, tc.expectedForeground, renderer.foregroundColor) } if tc.expectedBackground != nil { assert.Equal(t, tc.expectedBackground, renderer.backgroundColor) } }) } } func TestProcessAnsiSequence(t *testing.T) { cases := []struct { expectedForegroundColor *RGB expectedBackgroundColor *RGB colorOverrides map[string]HexColor name string ansiString string expectedAnsiString string expectedStyle string }{ { name: "Regular character", ansiString: "hello", expectedAnsiString: "hello", }, { name: "Inverted color", ansiString: "\x1b[38;2;255;0;0;49m\x1b[7mtest", expectedAnsiString: "test", expectedForegroundColor: &RGB{21, 21, 21}, // defaultBackgroundColor expectedBackgroundColor: &RGB{255, 0, 0}, }, { name: "Inverted color single", ansiString: "\x1b[31;49m\x1b[7mtest", expectedAnsiString: "test", expectedForegroundColor: &RGB{21, 21, 21}, // defaultBackgroundColor expectedBackgroundColor: &RGB{222, 56, 43}, // red background (31 + 10 = 41) }, { name: "Full color", ansiString: "\x1b[48;2;100;200;50m\x1b[38;2;255;0;0mtest", expectedAnsiString: "test", expectedBackgroundColor: &RGB{100, 200, 50}, expectedForegroundColor: &RGB{255, 0, 0}, }, { name: "Foreground color", ansiString: "\x1b[38;2;255;128;0mtest", expectedAnsiString: "test", expectedForegroundColor: &RGB{255, 128, 0}, }, { name: "Background color", ansiString: "\x1b[48;2;0;255;128mtest", expectedAnsiString: "test", expectedBackgroundColor: &RGB{0, 255, 128}, }, { name: "Reset sequence", ansiString: "\x1b[0mtest", expectedAnsiString: "test", expectedForegroundColor: &RGB{255, 255, 255}, // defaultForegroundColor expectedBackgroundColor: nil, }, { name: "Background reset", ansiString: "\x1b[49mtest", expectedAnsiString: "test", expectedBackgroundColor: nil, }, { name: "Bold style", ansiString: "\x1b[1mtest", expectedAnsiString: "test", expectedStyle: "bold", }, { name: "Italic style", ansiString: "\x1b[3mtest", expectedAnsiString: "test", expectedStyle: "italic", }, { name: "Underline style", ansiString: "\x1b[4mtest", expectedAnsiString: "test", expectedStyle: "underline", }, { name: "Overline style", ansiString: "\x1b[53mtest", expectedAnsiString: "test", expectedStyle: "overline", }, { name: "Bold reset", ansiString: "\x1b[22mtest", expectedAnsiString: "test", expectedStyle: "", }, { name: "Italic reset", ansiString: "\x1b[23mtest", expectedAnsiString: "test", expectedStyle: "", }, { name: "Underline reset", ansiString: "\x1b[24mtest", expectedAnsiString: "test", expectedStyle: "", }, { name: "Overline reset", ansiString: "\x1b[55mtest", expectedAnsiString: "test", expectedStyle: "", }, { name: "Strikethrough", ansiString: "\x1b[9mtest", expectedAnsiString: "test", }, { name: "Strikethrough reset", ansiString: "\x1b[29mtest", expectedAnsiString: "test", }, { name: "Left cursor movement", ansiString: "\x1b[5Dtest", expectedAnsiString: "test", }, { name: "Line change", ansiString: "\x1b[2Ftest", expectedAnsiString: "test", }, { name: "Console title", ansiString: "\x1b]0;My Title\007test", expectedAnsiString: "test", }, { name: "Base16 red color", ansiString: "\x1b[31mtest", expectedAnsiString: "test", expectedForegroundColor: &RGB{222, 56, 43}, }, { name: "Base16 blue background", ansiString: "\x1b[44mtest", expectedAnsiString: "test", expectedBackgroundColor: &RGB{0, 111, 184}, }, { name: "Base16 red with override", ansiString: "\x1b[31mtest", expectedAnsiString: "test", expectedForegroundColor: &RGB{255, 107, 107}, colorOverrides: map[string]HexColor{"red": "#FF6B6B"}, }, { name: "Link sequence", ansiString: "\x1b]8;;https://example.com\x1b\\Click here\x1b]8;;\x1b\\test", expectedAnsiString: "Click heretest", }, { name: "No matching sequence", ansiString: "plain text", expectedAnsiString: "plain text", }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { renderer := &Renderer{ AnsiString: tc.ansiString, Settings: Settings{ Colors: tc.colorOverrides, }, } renderer.initDefaults() var result bool for !result { result = renderer.processAnsiSequence() } assert.Equal(t, tc.expectedAnsiString, renderer.AnsiString) if tc.expectedForegroundColor != nil { assert.Equal(t, tc.expectedForegroundColor, renderer.foregroundColor) } if tc.expectedBackgroundColor != nil { assert.Equal(t, tc.expectedBackgroundColor, renderer.backgroundColor) } if tc.expectedStyle != "" { assert.Equal(t, tc.expectedStyle, renderer.style) } }) } } ================================================ FILE: src/cli/init.go ================================================ package cli import ( "fmt" "os" "path/filepath" "strings" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/config" "github.com/jandedobbeleer/oh-my-posh/src/log" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/runtime/path" "github.com/jandedobbeleer/oh-my-posh/src/shell" "github.com/jandedobbeleer/oh-my-posh/src/template" "github.com/spf13/cobra" "github.com/spf13/pflag" ) var ( printOutput bool strict bool debug bool supportedShells = []string{ "bash", "zsh", "fish", "powershell", "pwsh", "cmd", "nu", "elvish", "xonsh", } initCmd = createInitCmd() ) func init() { RootCmd.AddCommand(initCmd) } func createInitCmd() *cobra.Command { initCmd := &cobra.Command{ Use: "init [bash|zsh|fish|powershell|pwsh|cmd|nu|elvish|xonsh]", Short: "Initialize your shell and config", Long: `Initialize your shell and config. See the documentation to initialize your shell: https://ohmyposh.dev/docs/installation/prompt.`, ValidArgs: supportedShells, Args: NoArgsOrOneValidArg, Run: func(cmd *cobra.Command, args []string) { if len(args) == 0 { _ = cmd.Help() return } runInit(args[0], getFullCommand(cmd, args)) }, } initCmd.Flags().BoolVarP(&printOutput, "print", "p", false, "print the init script") initCmd.Flags().BoolVarP(&strict, "strict", "s", false, "run in strict mode") initCmd.Flags().BoolVar(&debug, "debug", false, "enable/disable debug mode") initCmd.Flags().BoolVar(&eval, "eval", false, "output the full init script for eval") _ = initCmd.MarkPersistentFlagRequired("config") return initCmd } func runInit(sh, command string) { if os.Getenv("CURSOR_AGENT") == "1" { log.Errorf("oh-my-posh init is disabled when running inside Cursor agent mode") return } if debug { log.Enable(plain) } if sh == "powershell" { sh = shell.PWSH } initCache(sh) cfg := config.Load(configFlag) flags := &runtime.Flags{ Shell: sh, ConfigPath: cfg.Source, ConfigHash: cfg.Hash(), Strict: strict, Debug: debug, Init: true, Eval: eval, Plain: plain, } env := &runtime.Terminal{} env.Init(flags) template.Init(env, cfg.Var, cfg.Maps) defer func() { cfg.Store() template.SaveCache() if err := cache.Clear(false, shell.InitScriptName(env.Flags())); err != nil { log.Error(err) } cache.Close() }() feats := cfg.Features(env) var output string switch { case debug: output = shell.Debug(env, feats, &startTime) case printOutput: output = shell.Script(env, feats) default: output = shell.Init(env, feats) } shellDSC := shell.DSC() shellDSC.Load() shellDSC.Add(&shell.Shell{ Command: command, Name: sh, }) shellDSC.Save() if silent { return } fmt.Print(output) } func getFullCommand(cmd *cobra.Command, args []string) string { // Start with the command path cmdPath := cmd.CommandPath() // Add arguments if len(args) > 0 { cmdPath += " " + strings.Join(args, " ") } // Add flags that were actually set cmd.Flags().VisitAll(func(flag *pflag.Flag) { if !flag.Changed { return } if flag.Value.Type() == "bool" && flag.Value.String() == "true" { cmdPath += fmt.Sprintf(" --%s", flag.Name) return } if flag.Name == "config" { configPath := filepath.Clean(flag.Value.String()) configPath = strings.ReplaceAll(configPath, path.Home(), "~") cmdPath += fmt.Sprintf(" --%s=%s", flag.Name, configPath) return } cmdPath += fmt.Sprintf(" --%s=%s", flag.Name, flag.Value.String()) }) return cmdPath } func initCache(sh string) { switch { case !printOutput: if (eval && sh == shell.PWSH) || sh == shell.ELVISH { cache.Init(sh) return } fallthrough default: cache.Init(sh, cache.NewSession, cache.Persist) } } ================================================ FILE: src/cli/notice.go ================================================ package cli import ( "fmt" "os" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/config" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/spf13/cobra" ) // noticeCmd represents the notice command var noticeCmd = &cobra.Command{ Use: "notice", Short: "Print the upgrade notice when a new version is available.", Long: "Print the upgrade notice when a new version is available.", Args: cobra.NoArgs, Run: func(_ *cobra.Command, _ []string) { env := &runtime.Terminal{} env.Init(&runtime.Flags{}) cache.Init(os.Getenv("POSH_SHELL"), cache.Persist) defer func() { cache.Close() }() cfg := config.Get(configFlag, false) if notice, hasNotice := cfg.Upgrade.Notice(); hasNotice { fmt.Println(notice) } }, } func init() { RootCmd.AddCommand(noticeCmd) } ================================================ FILE: src/cli/print.go ================================================ package cli import ( "fmt" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/prompt" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/shell" "github.com/jandedobbeleer/oh-my-posh/src/template" "github.com/spf13/cobra" ) var ( pwd string pswd string status int pipestatus string timing float64 stackCount int terminalWidth int eval bool cleared bool jobCount int saveCache bool command string shellVersion string plain bool noStatus bool column int escape bool ) // printCmd represents the print command var printCmd = createPrintCmd() func init() { RootCmd.AddCommand(printCmd) } func createPrintCmd() *cobra.Command { printCmd := &cobra.Command{ Use: "print [debug|primary|secondary|transient|right|tooltip|valid|error|preview]", Short: "Print the prompt/context", Long: "Print one of the prompts based on the location/use-case.", ValidArgs: []string{ prompt.DEBUG, prompt.PRIMARY, prompt.SECONDARY, prompt.TRANSIENT, prompt.RIGHT, prompt.TOOLTIP, prompt.VALID, prompt.ERROR, prompt.PREVIEW, }, Args: NoArgsOrOneValidArg, Run: func(cmd *cobra.Command, args []string) { if len(args) == 0 { _ = cmd.Help() return } if shellName == "" { shellName = shell.GENERIC } flags := &runtime.Flags{ ConfigPath: configFlag, PWD: pwd, PSWD: pswd, ErrorCode: status, PipeStatus: pipestatus, ExecutionTime: timing, StackCount: stackCount, TerminalWidth: terminalWidth, Eval: eval, Shell: shellName, ShellVersion: shellVersion, Plain: plain, Type: args[0], Cleared: cleared, NoExitCode: noStatus, Column: column, JobCount: jobCount, IsPrimary: args[0] == prompt.PRIMARY, Escape: escape, Force: force, } options := []cache.Option{} if saveCache { options = append(options, cache.Persist) } cache.Init(shellName, options...) eng := prompt.New(flags) defer func() { template.SaveCache() cache.Close() }() switch args[0] { case prompt.DEBUG: fmt.Print(eng.ExtraPrompt(prompt.Debug)) case prompt.PRIMARY: fmt.Print(eng.Primary()) case prompt.SECONDARY: fmt.Print(eng.ExtraPrompt(prompt.Secondary)) case prompt.TRANSIENT: fmt.Print(eng.ExtraPrompt(prompt.Transient)) case prompt.RIGHT: fmt.Print(eng.RPrompt()) case prompt.TOOLTIP: fmt.Print(eng.Tooltip(command)) case prompt.VALID: fmt.Print(eng.ExtraPrompt(prompt.Valid)) case prompt.ERROR: fmt.Print(eng.ExtraPrompt(prompt.Error)) case prompt.PREVIEW: fmt.Print(eng.Preview()) default: _ = cmd.Help() } }, } printCmd.Flags().StringVar(&pwd, "pwd", "", "current working directory") printCmd.Flags().StringVar(&pswd, "pswd", "", "current working directory (according to pwsh)") printCmd.Flags().StringVar(&shellName, "shell", "", "the shell to print for") printCmd.Flags().StringVar(&shellVersion, "shell-version", "", "the shell version") printCmd.Flags().IntVar(&status, "status", 0, "last known status code") printCmd.Flags().BoolVar(&noStatus, "no-status", false, "no valid status code (cancelled or no command yet)") printCmd.Flags().StringVar(&pipestatus, "pipestatus", "", "the PIPESTATUS array") printCmd.Flags().Float64Var(&timing, "execution-time", 0, "timing of the last command") printCmd.Flags().IntVarP(&stackCount, "stack-count", "s", 0, "number of locations on the stack") printCmd.Flags().IntVarP(&terminalWidth, "terminal-width", "w", 0, "width of the terminal") printCmd.Flags().StringVar(&command, "command", "", "tooltip command") printCmd.Flags().BoolVar(&cleared, "cleared", false, "do we have a clear terminal or not") printCmd.Flags().BoolVar(&eval, "eval", false, "output the prompt for eval") printCmd.Flags().IntVar(&column, "column", 0, "the column position of the cursor") printCmd.Flags().IntVar(&jobCount, "job-count", 0, "number of background jobs") printCmd.Flags().BoolVar(&saveCache, "save-cache", false, "save updated cache to file") printCmd.Flags().BoolVar(&escape, "escape", true, "escape the ANSI sequences for the shell") printCmd.Flags().BoolVarP(&force, "force", "f", false, "force rendering the segments") // Hide flags that are for internal use only. _ = printCmd.Flags().MarkHidden("save-cache") return printCmd } ================================================ FILE: src/cli/progress/model.go ================================================ package progress import ( "github.com/charmbracelet/bubbles/progress" tea "github.com/charmbracelet/bubbletea" "github.com/jandedobbeleer/oh-my-posh/src/terminal" ) type Message float64 func NewModel() *Model { p := progress.New(progress.WithScaledGradient("#800080", "#ffc0cb")) return &Model{Model: p} } type Model struct { progress.Model } func (m *Model) Update(msg tea.Msg) tea.Cmd { model, cmd := m.Model.Update(msg) m.Model = model.(progress.Model) return cmd } func (m *Model) View() string { return m.Model.View() + terminal.SetProgress(int(m.Percent()*100)) } ================================================ FILE: src/cli/progress/reader.go ================================================ package progress import ( "io" tea "github.com/charmbracelet/bubbletea" ) func NewReader(reader io.Reader, total int64, program *tea.Program) *Reader { return &Reader{ Reader: reader, program: program, total: total, } } type Reader struct { io.Reader program *tea.Program total int64 current int64 } func (r *Reader) Read(p []byte) (int, error) { n, err := r.Reader.Read(p) r.current += int64(n) percent := float64(r.current) / float64(r.total) if r.program != nil { r.program.Send(Message(percent)) } return n, err } ================================================ FILE: src/cli/root.go ================================================ package cli import ( "fmt" "os" "path/filepath" "strings" "time" "github.com/jandedobbeleer/oh-my-posh/src/build" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/log" "github.com/spf13/cobra" ) var ( configFlag string shellName string printVersion bool trace bool exitcode int // for internal use only silent bool // deprecated initialize bool ) var RootCmd = &cobra.Command{ Use: "oh-my-posh", Short: "oh-my-posh is a tool to render your prompt", Long: `oh-my-posh is a cross platform tool to render your prompt. It can use the same configuration everywhere to offer a consistent experience, regardless of where you are. For a detailed guide on getting started, have a look at the docs at https://ohmyposh.dev`, Run: func(cmd *cobra.Command, args []string) { if initialize { runInit(strings.ToLower(shellName), getFullCommand(cmd, args)) return } if printVersion { fmt.Println(build.Version) return } _ = cmd.Help() }, PersistentPreRun: func(cmd *cobra.Command, args []string) { configEnv := os.Getenv("POSH_CONFIG") if configEnv != "" && configFlag == "" { configFlag = configEnv } traceEnv := os.Getenv("POSH_TRACE") if traceEnv == "" && !trace { return } trace = true log.Enable(true) log.Debug("version:", build.Version) log.Debug("command:", getFullCommand(cmd, args)) }, PersistentPostRun: func(cmd *cobra.Command, args []string) { defer func() { if exitcode != 0 { os.Exit(exitcode) } }() if !trace { return } var prefix string if shellName != "" { prefix = fmt.Sprintf("%s-", shellName) } cli := append([]string{cmd.Name()}, args...) filename := fmt.Sprintf("%s-%s%s.log", time.Now().Format("02012006T150405.000"), prefix, strings.Join(cli, "-")) logPath := filepath.Join(cache.Path(), "logs") err := os.MkdirAll(logPath, 0755) if err != nil { return } err = os.WriteFile(filepath.Join(logPath, filename), []byte(log.String()), 0644) if err != nil { return } }, } func Execute() { if err := RootCmd.Execute(); err != nil { // software error os.Exit(70) } } func init() { RootCmd.PersistentFlags().StringVarP(&configFlag, "config", "c", "", "config file path") RootCmd.PersistentFlags().BoolVar(&silent, "silent", false, "do not print anything") RootCmd.PersistentFlags().BoolVar(&trace, "trace", false, "enable tracing") RootCmd.PersistentFlags().BoolVar(&plain, "plain", false, "plain text output (no ANSI)") RootCmd.Flags().BoolVar(&printVersion, "version", false, "print the version number and exit") // Deprecated flags, should be kept to avoid breaking CLI integration. RootCmd.Flags().BoolVarP(&initialize, "init", "i", false, "init") RootCmd.Flags().StringVarP(&shellName, "shell", "s", "", "shell") // Hide flags that are deprecated or for internal use only. _ = RootCmd.PersistentFlags().MarkHidden("silent") // Disable completions RootCmd.CompletionOptions.DisableDefaultCmd = true } ================================================ FILE: src/cli/shell.go ================================================ package cli import ( "fmt" "os" "github.com/jandedobbeleer/oh-my-posh/src/dsc" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/shell" "github.com/spf13/cobra" ) // shellCmd represents the shell command var shellCmd = &cobra.Command{ Use: "shell get", Short: "Get the shell name", Long: `Get the shell name. This command retrieves the name of the current shell being used.`, Example: ` oh-my-posh shell get`, ValidArgs: []string{ "get", }, Args: NoArgsOrOneValidArg, Run: func(cmd *cobra.Command, args []string) { if len(args) == 0 { _ = cmd.Help() return } flags := &runtime.Flags{ Shell: os.Getenv("POSH_SHELL"), } env := &runtime.Terminal{} env.Init(flags) switch args[0] { case "get": fmt.Print(env.Shell()) default: _ = cmd.Help() } }, } func init() { shellCmd.AddCommand(dsc.Command(shell.DSC())) RootCmd.AddCommand(shellCmd) } ================================================ FILE: src/cli/stream.go ================================================ package cli import ( "fmt" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/prompt" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/shell" "github.com/jandedobbeleer/oh-my-posh/src/template" "github.com/spf13/cobra" ) // streamCmd represents the stream command var streamCmd = createStreamCmd() func init() { RootCmd.AddCommand(streamCmd) } func createStreamCmd() *cobra.Command { streamCmd := &cobra.Command{ Use: "stream", Short: "Stream the prompt with incremental updates", Long: `Stream the primary prompt with incremental updates as segments complete. Output format: null-byte delimited prompt strings (each complete prompt separated by \0). This allows multi-line prompts to be handled correctly. The shell can read records incrementally and update the display. Command exits when all segments are resolved.`, Args: cobra.NoArgs, Run: func(_ *cobra.Command, _ []string) { if shellName == "" { shellName = shell.GENERIC } flags := &runtime.Flags{ ConfigPath: configFlag, PWD: pwd, PSWD: pswd, ErrorCode: status, PipeStatus: pipestatus, ExecutionTime: timing, StackCount: stackCount, TerminalWidth: terminalWidth, Eval: eval, Shell: shellName, ShellVersion: shellVersion, Plain: plain, Type: prompt.PRIMARY, Cleared: cleared, NoExitCode: noStatus, Column: column, JobCount: jobCount, IsPrimary: true, Escape: escape, Force: force, Streaming: true, } options := []cache.Option{} if saveCache { options = append(options, cache.Persist) } cache.Init(shellName, options...) eng := prompt.New(flags) defer func() { template.SaveCache() cache.Close() }() // Stream prompt updates for promptString := range eng.StreamPrimary() { fmt.Print(promptString) fmt.Print("\x00") // Null byte delimiter for multi-line prompts } }, } streamCmd.Flags().StringVar(&pwd, "pwd", "", "current working directory") streamCmd.Flags().StringVar(&pswd, "pswd", "", "current working directory (according to pwsh)") streamCmd.Flags().StringVar(&shellName, "shell", "", "the shell to stream for") streamCmd.Flags().StringVar(&shellVersion, "shell-version", "", "the shell version") streamCmd.Flags().IntVar(&status, "status", 0, "last known status code") streamCmd.Flags().BoolVar(&noStatus, "no-status", false, "no valid status code (cancelled or no command yet)") streamCmd.Flags().StringVar(&pipestatus, "pipestatus", "", "the PIPESTATUS array") streamCmd.Flags().Float64Var(&timing, "execution-time", 0, "timing of the last command") streamCmd.Flags().IntVarP(&stackCount, "stack-count", "s", 0, "number of locations on the stack") streamCmd.Flags().IntVarP(&terminalWidth, "terminal-width", "w", 0, "width of the terminal") streamCmd.Flags().BoolVar(&cleared, "cleared", false, "do we have a clear terminal or not") streamCmd.Flags().BoolVar(&eval, "eval", false, "output the prompt for eval") streamCmd.Flags().IntVar(&column, "column", 0, "the column position of the cursor") streamCmd.Flags().IntVar(&jobCount, "job-count", 0, "number of background jobs") streamCmd.Flags().BoolVar(&saveCache, "save-cache", false, "save updated cache to file") streamCmd.Flags().BoolVar(&escape, "escape", true, "escape the ANSI sequences for the shell") streamCmd.Flags().BoolVarP(&force, "force", "f", false, "force rendering the segments") // Hide flags that are for internal use only. _ = streamCmd.Flags().MarkHidden("save-cache") return streamCmd } ================================================ FILE: src/cli/stream_test.go ================================================ package cli import ( "bytes" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestStreamCommand_Creation(t *testing.T) { cmd := createStreamCmd() assert.NotNil(t, cmd) assert.Equal(t, "stream", cmd.Use) assert.Equal(t, "Stream the prompt with incremental updates", cmd.Short) } func TestStreamCommand_Flags(t *testing.T) { cmd := createStreamCmd() // Verify all expected flags exist expectedFlags := []string{ "pwd", "pswd", "shell", "shell-version", "status", "no-status", "pipestatus", "execution-time", "stack-count", "terminal-width", "cleared", "eval", "column", "job-count", "save-cache", "escape", "force", } for _, flagName := range expectedFlags { flag := cmd.Flags().Lookup(flagName) assert.NotNil(t, flag, "Flag '%s' should exist", flagName) } } func TestStreamCommand_RequiredFlagsForStreaming(t *testing.T) { // This test validates that the stream command sets the correct flags // for streaming execution mode cmd := createStreamCmd() // Verify that running the command would set streaming=true // We can't easily test the actual run without a full config, // but we can verify the command is properly configured assert.NotNil(t, cmd.Run) assert.NotNil(t, cmd.Args) } func TestStreamCommand_FlagInheritance(t *testing.T) { // Verify that stream command uses the same flags as print command // This ensures consistency between commands streamCmd := createStreamCmd() printCmd := createPrintCmd() // Core flags that should exist in both sharedFlags := []string{ "pwd", "shell", "status", "execution-time", "terminal-width", "eval", "force", } for _, flagName := range sharedFlags { streamFlag := streamCmd.Flags().Lookup(flagName) printFlag := printCmd.Flags().Lookup(flagName) assert.NotNil(t, streamFlag, "Stream command should have '%s' flag", flagName) assert.NotNil(t, printFlag, "Print command should have '%s' flag", flagName) // Verify default values match if streamFlag != nil && printFlag != nil { assert.Equal(t, printFlag.DefValue, streamFlag.DefValue, "Flag '%s' should have same default value in both commands", flagName) } } } func TestStreamCommand_OutputDelimiter(t *testing.T) { // Test that output uses null byte delimiter for multi-line prompts tests := []struct { name string expected string prompts []string }{ { name: "Single line prompt", prompts: []string{"prompt1"}, expected: "prompt1\x00", }, { name: "Multi-line prompt", prompts: []string{"line1\nline2\nline3"}, expected: "line1\nline2\nline3\x00", }, { name: "Multiple prompts", prompts: []string{"prompt1", "prompt2", "prompt3"}, expected: "prompt1\x00prompt2\x00prompt3\x00", }, { name: "Multiple multi-line prompts", prompts: []string{"line1\nline2", "line3\nline4"}, expected: "line1\nline2\x00line3\nline4\x00", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Simulate output with null byte delimiter var buf bytes.Buffer for _, prompt := range tt.prompts { buf.WriteString(prompt) buf.WriteString("\x00") } assert.Equal(t, tt.expected, buf.String()) }) } } func TestStreamCommand_Integration_MockOutput(t *testing.T) { // This test validates the output structure without requiring a full engine // It simulates what the stream command would output with null byte delimiter tests := []struct { validateOutput func(t *testing.T, output string) name string promptCount int }{ { name: "Single prompt with null byte", promptCount: 1, validateOutput: func(t *testing.T, output string) { assert.True(t, strings.HasSuffix(output, "\x00")) }, }, { name: "Multiple prompts with null bytes", promptCount: 3, validateOutput: func(t *testing.T, output string) { parts := strings.Split(output, "\x00") // 3 prompts = 4 parts (including trailing empty string after last \x00) assert.Len(t, parts, 4) // Last part should be empty assert.Equal(t, "", parts[3]) }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Simulate stream output with null byte delimiter var output bytes.Buffer for i := 0; i < tt.promptCount; i++ { output.WriteString("prompt") output.WriteString("\x00") } tt.validateOutput(t, output.String()) }) } } func TestStreamCommand_HiddenFlags(t *testing.T) { cmd := createStreamCmd() // Verify save-cache is hidden (internal use only) saveCacheFlag := cmd.Flags().Lookup("save-cache") require.NotNil(t, saveCacheFlag) assert.True(t, saveCacheFlag.Hidden, "save-cache flag should be hidden") } func TestStreamCommand_NoArgs(t *testing.T) { cmd := createStreamCmd() // Stream command should not accept positional arguments // (unlike print which accepts primary/secondary/etc.) assert.NotNil(t, cmd.Args) // Test that NoArgs validator rejects arguments err := cmd.Args(cmd, []string{"extra"}) assert.Error(t, err, "Should reject arguments when NoArgs is used") // Test that NoArgs validator accepts no arguments err = cmd.Args(cmd, []string{}) assert.NoError(t, err, "Should accept no arguments") } func TestStreamCommand_StreamingFlagEnabled(t *testing.T) { // This validates that the stream command would create // a Flags struct with Streaming=true // We can't easily test the full execution without mocking the entire engine, // but we can verify the command structure is correct cmd := createStreamCmd() assert.NotNil(t, cmd.Run) // The Run function should: // 1. Create Flags with Streaming=true // 2. Set Type=prompt.PRIMARY // 3. Set IsPrimary=true // These are validated by code inspection in the createStreamCmd implementation } ================================================ FILE: src/cli/toggle.go ================================================ package cli import ( "os" "strings" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/spf13/cobra" ) // toggleCmd represents the toggle command var toggleCmd = &cobra.Command{ Use: "toggle segment1 segment2 ...", Short: "Toggle one or more segments on/off", Long: "Toggle one or more segments on/off on the fly. Multiple segments can be specified separated by spaces.", Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { if len(args) == 0 { _ = cmd.Help() return } env := &runtime.Terminal{} env.Init(&runtime.Flags{}) cache.Init(os.Getenv("POSH_SHELL"), cache.Persist) defer func() { cache.Close() }() // Get current toggles from cache as a map currentToggleSet, _ := cache.Get[map[string]bool](cache.Session, cache.TOGGLECACHE) if currentToggleSet == nil { currentToggleSet = make(map[string]bool) } segmentsToToggle := parseSegments(args) // Toggle segments: remove if present, add if not present for _, segment := range segmentsToToggle { if currentToggleSet[segment] { delete(currentToggleSet, segment) continue } currentToggleSet[segment] = true } // Store the map directly in cache cache.Set(cache.Session, cache.TOGGLECACHE, currentToggleSet, cache.INFINITE) }, } func parseSegments(args []string) []string { var segments []string for _, arg := range args { if segment := strings.TrimSpace(arg); segment != "" { segments = append(segments, segment) } } return segments } func init() { RootCmd.AddCommand(toggleCmd) } ================================================ FILE: src/cli/upgrade/config.go ================================================ package upgrade import ( "context" "encoding/gob" "fmt" "io" httplib "net/http" "strings" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/cli/progress" "github.com/jandedobbeleer/oh-my-posh/src/log" "github.com/jandedobbeleer/oh-my-posh/src/runtime/http" ) func init() { gob.Register(&Config{}) gob.Register((*Source)(nil)) } type Config struct { Source Source `json:"source" toml:"source" yaml:"source"` Interval cache.Duration `json:"interval" toml:"interval" yaml:"interval"` Latest string `json:"-" toml:"-" yaml:"-"` Auto bool `json:"auto" toml:"auto" yaml:"auto"` DisplayNotice bool `json:"notice" toml:"notice" yaml:"notice"` Force bool `json:"-" toml:"-" yaml:"-"` } type Source string const ( GitHub Source = "github" CDN Source = "cdn" ) func (s Source) String() string { switch s { case GitHub: return "github.com" case CDN: return "cdn.ohmyposh.dev" default: return "Unknown" } } func (cfg *Config) FetchLatest() (string, error) { cfg.Latest = "latest" v, err := cfg.DownloadAsset("version.txt") if err != nil { log.Debugf("failed to get latest version for source: %s", cfg.Source) return "", err } version := strings.TrimSpace(string(v)) cfg.Latest = version version = strings.TrimPrefix(version, "v") log.Debugf("latest version: %s", version) return version, err } func (cfg *Config) DownloadAsset(asset string) ([]byte, error) { if cfg.Source == "" { log.Debug("no source specified, defaulting to github") cfg.Source = GitHub } switch cfg.Source { case GitHub: var url string switch cfg.Latest { case "latest": url = fmt.Sprintf("https://github.com/JanDeDobbeleer/oh-my-posh/releases/latest/download/%s", asset) default: url = fmt.Sprintf("https://github.com/JanDeDobbeleer/oh-my-posh/releases/download/%s/%s", cfg.Latest, asset) } return cfg.Download(url) case CDN: fallthrough default: url := fmt.Sprintf("https://cdn.ohmyposh.dev/releases/%s/%s", cfg.Latest, asset) return cfg.Download(url) } } func (cfg *Config) Download(url string) ([]byte, error) { req, err := httplib.NewRequestWithContext(context.Background(), "GET", url, nil) if err != nil { log.Debugf("failed to create request for url: %s", url) return nil, err } req.Header.Add("User-Agent", "oh-my-posh") req.Header.Add("Cache-Control", "max-age=0") resp, err := http.HTTPClient.Do(req) if err != nil { log.Debugf("failed to execute HTTP request: %s", url) return nil, err } if resp.StatusCode != httplib.StatusOK { return nil, fmt.Errorf("failed to download asset: %s", url) } defer resp.Body.Close() reader := progress.NewReader(resp.Body, resp.ContentLength, program) data, err := io.ReadAll(reader) if err != nil { log.Debugf("failed to read response body: %s", url) return nil, err } return data, nil } ================================================ FILE: src/cli/upgrade/install.go ================================================ package upgrade import ( "bytes" "errors" "fmt" "io" "os" "path/filepath" "github.com/jandedobbeleer/oh-my-posh/src/log" ) func install(cfg *Config) error { setState(validating) executable, err := os.Executable() if err != nil { log.Debug("failed to get executable path") return err } targetDir := filepath.Dir(executable) fileName := filepath.Base(executable) newPath := filepath.Join(targetDir, fmt.Sprintf(".%s.new", fileName)) fp, err := os.OpenFile(newPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0775) if err != nil { log.Error(err) return errors.New("we do not have permissions to update") } setState(downloading) data, err := downloadAndVerify(cfg) if err != nil { log.Debug("failed to download and verify") return err } setState(installing) _, err = io.Copy(fp, bytes.NewReader(data)) // windows will have a lock when we do not close the file fp.Close() if err != nil { log.Debug("failed to copy data to new file") return err } oldPath := filepath.Join(targetDir, fmt.Sprintf(".%s.old", fileName)) _ = os.Remove(oldPath) err = os.Rename(executable, oldPath) if err != nil { log.Debug("failed to rename old file") return err } err = os.Rename(newPath, executable) if err != nil { log.Debug("failed to rename new file, rolling back") // rollback rerr := os.Rename(oldPath, executable) if rerr != nil { log.Debug("failed to rollback old file") return rerr } return err } removeErr := os.Remove(oldPath) // hide the old executable if we can't remove it if removeErr != nil { log.Error(removeErr) // hide the old executable _ = hideFile(oldPath) } return nil } ================================================ FILE: src/cli/upgrade/install_noop.go ================================================ //go:build !windows package upgrade func hideFile(_ string) error { return nil } func IsPackagedInstallation() bool { return false } ================================================ FILE: src/cli/upgrade/install_windows.go ================================================ package upgrade import ( "syscall" "unsafe" "github.com/jandedobbeleer/oh-my-posh/src/cache" ) func hideFile(path string) error { kernel32 := syscall.NewLazyDLL("kernel32.dll") setFileAttributes := kernel32.NewProc("SetFileAttributesW") ptr, err := syscall.UTF16PtrFromString(path) if err != nil { return err } r1, _, err := setFileAttributes.Call(uintptr(unsafe.Pointer(ptr)), 2) if r1 == 0 { return err } return nil } func IsPackagedInstallation() bool { _, ok := cache.PackageFamilyName() return ok } ================================================ FILE: src/cli/upgrade/notice.go ================================================ package upgrade import ( "fmt" "os" "github.com/jandedobbeleer/oh-my-posh/src/build" "github.com/jandedobbeleer/oh-my-posh/src/log" "github.com/jandedobbeleer/oh-my-posh/src/runtime/http" ) const ( CACHEKEY = "upgrade_check" upgradeNotice = ` A new release of Oh My Posh is available: v%s → v%s To upgrade, run: 'oh-my-posh upgrade%s' To enable automated upgrades, run: 'oh-my-posh enable upgrade'. ` ) // Returns the upgrade notice if a new version is available // that should be displayed to the user. // // The upgrade check is only performed every other week. func (cfg *Config) Notice() (string, bool) { // never validate when we install using the Windows Store if os.Getenv("POSH_INSTALLER") == "ws" { log.Debug("skipping upgrade check because we are using the Windows Store") return "", false } if !http.IsConnected() { return "", false } latest, err := cfg.FetchLatest() if err != nil { return "", false } if latest == build.Version { return "", false } var forceUpdate string if IsMajorUpgrade(build.Version, latest) { forceUpdate = " --force" } return fmt.Sprintf(upgradeNotice, build.Version, latest, forceUpdate), true } ================================================ FILE: src/cli/upgrade/notice_test.go ================================================ package upgrade import ( "os" "testing" "github.com/jandedobbeleer/oh-my-posh/src/build" "github.com/stretchr/testify/assert" ) func TestCanUpgrade(t *testing.T) { ugc := &Config{} latest, _ := ugc.FetchLatest() cases := []struct { Case string CurrentVersion string Installer string Expected bool Cache bool }{ {Case: "Up to date", CurrentVersion: latest}, {Case: "Outdated Linux", Expected: true, CurrentVersion: "3.0.0"}, {Case: "Outdated Darwin", Expected: true, CurrentVersion: "3.0.0"}, {Case: "Cached", Cache: true, CurrentVersion: latest}, {Case: "Windows Store", Installer: "ws"}, } for _, tc := range cases { build.Version = tc.CurrentVersion if len(tc.Installer) > 0 { os.Setenv("POSH_INSTALLER", tc.Installer) } _, canUpgrade := ugc.Notice() assert.Equal(t, tc.Expected, canUpgrade, tc.Case) os.Setenv("POSH_INSTALLER", "") } } ================================================ FILE: src/cli/upgrade/public_key.pem ================================================ -----BEGIN PUBLIC KEY----- MCowBQYDK2VwAyEA98lHhNau5x0JtjSuwiWLuC2yKO6NA6/0bH2gE8tAq4c= -----END PUBLIC KEY----- ================================================ FILE: src/cli/upgrade/tui.go ================================================ package upgrade import ( "fmt" "strings" progress_ "github.com/charmbracelet/bubbles/progress" "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/jandedobbeleer/oh-my-posh/src/build" "github.com/jandedobbeleer/oh-my-posh/src/cli/progress" "github.com/jandedobbeleer/oh-my-posh/src/log" ) var ( program *tea.Program textStyle = lipgloss.NewStyle().Margin(1, 0, 2, 2) ) type resultMsg string type stateMsg state type state int const ( validating state = iota downloading verifying installing ) func setState(message state) { if program == nil { return } program.Send(stateMsg(message)) } type model struct { error error config *Config spinner *spinner.Model progress *progress.Model message string state state } func initialModel(cfg *Config) *model { s := spinner.New() s.Spinner = spinner.Dot s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("170")) p := progress.NewModel() return &model{spinner: &s, config: cfg, progress: p} } func (m *model) Init() tea.Cmd { go m.start() return m.spinner.Tick } func (m *model) start() { if err := install(m.config); err != nil { m.error = err log.Debug("failed to install") program.Send(resultMsg(fmt.Sprintf(" ❌ upgrade failed: %v", err))) return } current := fmt.Sprintf("v%s", build.Version) message := fmt.Sprintf("🚀 Upgraded from %s to %s", current, m.config.Latest) if current != m.config.Latest { log.Debug("new version installed, user needs to restart shell") message += ", restart your shell to take full advantage of the new functionality" } program.Send(resultMsg(message)) } func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "q", "esc", "ctrl+c": return m, tea.Quit default: return m, nil } case resultMsg: m.message = string(msg) return m, tea.Quit case stateMsg: m.state = state(msg) return m, nil case progress.Message: return m, m.progress.SetPercent(float64(msg)) case progress_.FrameMsg: return m, m.progress.Update(msg) default: s, cmd := m.spinner.Update(msg) m.spinner = &s return m, cmd } } func (m *model) View() string { if len(m.message) > 0 { return textStyle.Render(m.message) } var message string m.spinner.Spinner = spinner.Dot switch m.state { case validating: message = "Validating current installation" case downloading: message = fmt.Sprintf("Downloading %s from %s...\n%s", m.config.Latest, m.config.Source.String(), m.progress.View()) return textStyle.Render(message) case verifying: m.spinner.Spinner = spinner.Moon message = "Verifying download" case installing: m.spinner.Spinner = spinner.Jump message = "Installing" } return textStyle.Render(fmt.Sprintf("%s %s", m.spinner.View(), message)) } func Run(cfg *Config) error { program = tea.NewProgram(initialModel(cfg)) resultModel, _ := program.Run() programModel, OK := resultModel.(*model) if !OK { log.Debug("failed to cast model") return nil } return programModel.error } func IsMajorUpgrade(current, latest string) bool { if current == "" { return false } getMajorNumber := func(version string) string { major, _, _ := strings.Cut(version, ".") return major } return getMajorNumber(current) != getMajorNumber(latest) } ================================================ FILE: src/cli/upgrade/tui_test.go ================================================ package upgrade import ( "testing" "github.com/stretchr/testify/assert" ) func TestIsMajorUpgrade(t *testing.T) { cases := []struct { Case string CurrentVersion string LatestVersion string Expected bool }{ {Case: "Same version", Expected: false, CurrentVersion: "v3.0.0", LatestVersion: "v3.0.0"}, {Case: "Breaking change", Expected: true, CurrentVersion: "v3.0.0", LatestVersion: "v4.0.0"}, {Case: "Empty version, mostly development build", Expected: false, LatestVersion: "v4.0.0"}, } for _, tc := range cases { canUpgrade := IsMajorUpgrade(tc.CurrentVersion, tc.LatestVersion) assert.Equal(t, tc.Expected, canUpgrade, tc.Case) } } ================================================ FILE: src/cli/upgrade/verify.go ================================================ package upgrade import ( "crypto/ed25519" "crypto/sha256" "crypto/x509" _ "embed" "encoding/pem" "fmt" stdruntime "runtime" "strings" "github.com/jandedobbeleer/oh-my-posh/src/log" "github.com/jandedobbeleer/oh-my-posh/src/runtime" ) // This is based on the following key generation and validation. // Generate a private key: // openssl genpkey -algorithm Ed25519 -out private_key.pem // Extract the public key: // openssl pkey -in private_key.pem -pubout -out public_key.pem // Sign the checksums.txt file: // openssl pkeyutl -sign -inkey private_key.pem -out checksums.txt.sig -rawin -in checksums.txt // Verify the signature: // openssl pkeyutl -verify -pubin -inkey public_key.pem -sigfile checksums.txt.sig -rawin -in checksums.txt // The public key is embedded in the binary. // The private key is used to sign the checksums.txt file. // The signature is embedded in the release. // The checksums.txt file contains the checksums of the release assets. // All checks are done in memory. // Only then the binary is written to disk. //go:embed public_key.pem var publicKey []byte func downloadAndVerify(cfg *Config) ([]byte, error) { extension := "" if stdruntime.GOOS == runtime.WINDOWS { extension = ".exe" } asset := fmt.Sprintf("posh-%s-%s%s", stdruntime.GOOS, stdruntime.GOARCH, extension) log.Debug("downloading asset:", asset) data, err := cfg.DownloadAsset(asset) if err != nil { log.Debug("failed to download asset") return nil, err } setState(verifying) err = verify(cfg, asset, data) if err != nil { log.Debug("failed to verify asset") return nil, err } return data, nil } func verify(cfg *Config, asset string, binary []byte) error { checksums, err := cfg.DownloadAsset("checksums.txt") if err != nil { log.Debug("failed to download checksums") return err } signature, err := cfg.DownloadAsset("checksums.txt.sig") if err != nil { log.Debug("failed to download checksums signature") return err } OK := validateSignature(checksums, signature) if !OK { log.Debug("failed to verify checksums signature") return fmt.Errorf("failed to verify checksums signature") } return validateChecksum(asset, checksums, binary) } func validateSignature(data, signature []byte) bool { ed25519PublicKey, err := loadPublicKey() if err != nil { log.Debug("failed to load public key") log.Error(err) return false } return ed25519.Verify(*ed25519PublicKey, data, signature) } func loadPublicKey() (*ed25519.PublicKey, error) { block, _ := pem.Decode(publicKey) if block == nil { log.Debug("failed to decode PEM block") return nil, fmt.Errorf("error parsing PEM block: key not found") } pubKey, err := x509.ParsePKIXPublicKey(block.Bytes) if err != nil { log.Debug("failed to parse public key") return nil, fmt.Errorf("error parsing public key: %v", err) } ed25519PubKey, ok := pubKey.(ed25519.PublicKey) if !ok { log.Debug("failed to convert public key to ed25519") return nil, fmt.Errorf("invalid public key format: %v", err) } return &ed25519PubKey, nil } func validateChecksum(asset string, sha256sums, binary []byte) error { var assetChecksum string checksums := strings.SplitSeq(string(sha256sums), "\n") for line := range checksums { if !strings.HasSuffix(line, asset) { continue } assetChecksum = strings.Fields(line)[0] break } if assetChecksum == "" { log.Debug("failed to find checksum for asset") return fmt.Errorf("failed to find checksum for asset") } // calculate the checksum of the binary binaryChecksum := fmt.Sprintf("%x", sha256.Sum256(binary)) if assetChecksum != binaryChecksum { log.Debugf("checksum mismatch, expected: %s, got: %s", assetChecksum, binaryChecksum) return fmt.Errorf("checksum mismatch") } return nil } ================================================ FILE: src/cli/upgrade/verify_test.go ================================================ package upgrade import ( "os" "testing" "github.com/stretchr/testify/assert" ) func TestVerify(t *testing.T) { checksum, err := os.ReadFile("../../test/signing/checksums.txt") assert.NoError(t, err) signature, err := os.ReadFile("../../test/signing/checksums.txt.sig") assert.NoError(t, err) OK := validateSignature(checksum, signature) assert.True(t, OK) } func TestVerifyFail(t *testing.T) { checksum, err := os.ReadFile("../../test/signing/checksums.txt") assert.NoError(t, err) signature, err := os.ReadFile("../../test/signing/checksums.txt.invalid.sig") assert.NoError(t, err) OK := validateSignature(checksum, signature) assert.False(t, OK) } ================================================ FILE: src/cli/upgrade.go ================================================ package cli import ( "fmt" "os" stdruntime "runtime" "slices" "time" "github.com/jandedobbeleer/oh-my-posh/src/build" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/cli/upgrade" "github.com/jandedobbeleer/oh-my-posh/src/config" "github.com/jandedobbeleer/oh-my-posh/src/log" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/terminal" "github.com/jandedobbeleer/oh-my-posh/src/text" "github.com/spf13/cobra" ) var ( force bool auto bool ) // upgradeCmd represents the upgrade command var upgradeCmd = &cobra.Command{ Use: "upgrade", Short: "Upgrade when a new version is available.", Long: "Upgrade when a new version is available.", Args: cobra.NoArgs, Run: func(_ *cobra.Command, _ []string) { var startTime time.Time if debug { startTime = time.Now() log.Enable(plain) } if upgrade.IsPackagedInstallation() { msg := "upgrade is not supported when installed as a MSIX package" log.Debug(msg) fmt.Printf("\n ❌ %s\n\n", msg) return } supportedPlatforms := []string{ runtime.WINDOWS, runtime.DARWIN, runtime.LINUX, } if !slices.Contains(supportedPlatforms, stdruntime.GOOS) { log.Debug("unsupported platform") return } sh := os.Getenv("POSH_SHELL") env := &runtime.Terminal{} env.Init(&runtime.Flags{ Debug: debug, }) cache.Init(sh, cache.Persist) // Only respect the cache interval when using --auto flag if _, OK := cache.Get[string](cache.Device, upgrade.CACHEKEY); OK && auto { log.Debug("upgrade check already performed recently, skipping") return } terminal.Init(sh) fmt.Print(terminal.StartProgress()) cfg := config.Get(configFlag, false) defer func() { fmt.Print(terminal.StopProgress()) // Set the cache key after any upgrade check to prevent redundant checks cache.Set(cache.Device, upgrade.CACHEKEY, "true", cfg.Upgrade.Interval) cache.Close() if !debug { return } sb := text.NewBuilder() sb.WriteString(fmt.Sprintf("%s %s\n", log.Text("Upgrade duration:").Green().Bold().Plain(), time.Since(startTime))) sb.WriteString(log.Text("\nLogs:\n\n").Green().Bold().Plain().String()) sb.WriteString(env.Logs()) fmt.Println(sb.String()) }() latest, err := cfg.Upgrade.FetchLatest() if err != nil { log.Debug("failed to get latest version") log.Error(err) fmt.Printf("\n ❌ %s\n\n", err) exitcode = 1 return } log.Debugf("current version: v%s, latest version: v%s", build.Version, latest) if force { log.Debug("forced upgrade") exitcode = executeUpgrade(cfg.Upgrade) return } if upgrade.IsMajorUpgrade(build.Version, latest) { log.Debug("major upgrade available") message := fmt.Sprintf("\n 🚨 major upgrade available: v%s -> v%s, use oh-my-posh upgrade --force to upgrade\n\n", build.Version, latest) fmt.Print(message) return } if build.Version != latest { log.Debug("upgrade available") exitcode = executeUpgrade(cfg.Upgrade) return } log.Debug("already on the latest version") }, } func executeUpgrade(cfg *upgrade.Config) int { err := upgrade.Run(cfg) if err == nil { return 0 } log.Debug("failed to upgrade") log.Error(err) return 1 } func init() { upgradeCmd.Flags().BoolVarP(&force, "force", "f", false, "force the upgrade even if the version is up to date") upgradeCmd.Flags().BoolVar(&auto, "auto", false, "respect the cache interval for automatic upgrades") upgradeCmd.Flags().BoolVar(&debug, "debug", false, "enable/disable debug mode") RootCmd.AddCommand(upgradeCmd) } ================================================ FILE: src/cli/version.go ================================================ package cli import ( "fmt" "github.com/jandedobbeleer/oh-my-posh/src/build" "github.com/spf13/cobra" ) var ( verbose bool ) // versionCmd represents the version command var versionCmd = &cobra.Command{ Use: "version", Short: "Print the version", Long: "Print the version number of oh-my-posh.", Args: cobra.NoArgs, Run: func(_ *cobra.Command, _ []string) { if !verbose { fmt.Println(build.Version) return } fmt.Println("Version: ", build.Version) fmt.Println("Date: ", build.Date) }, } func init() { versionCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "write verbose output") RootCmd.AddCommand(versionCmd) } ================================================ FILE: src/color/colors.go ================================================ package color import ( "encoding/gob" "fmt" "strconv" "strings" "time" "github.com/gookit/color" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/generics" "github.com/jandedobbeleer/oh-my-posh/src/log" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/template" ) func init() { gob.Register(&Set{}) gob.Register((*Ansi)(nil)) gob.Register(&Palette{}) gob.Register(&Palettes{}) gob.Register(&Cycle{}) } const ( accentColor = "accent_color" ) var TrueColor = true // String is the interface that wraps ToColor method. // // ToColor gets the ANSI color code for a given color string. // This can include a valid hex color in the format `#FFFFFF`, // but also a name of one of the first 16 ANSI colors like `lightBlue`. type String interface { ToAnsi(colorString Ansi, isBackground bool) Ansi Resolve(colorString Ansi) (Ansi, error) } type Set struct { Background Ansi `json:"background" toml:"background" yaml:"background"` Foreground Ansi `json:"foreground" toml:"foreground" yaml:"foreground"` } func (c *Set) String() string { return fmt.Sprintf("%s|%s", c.Foreground, c.Background) } func (c *Set) ParseString(colors string) { parts := strings.SplitN(colors, "|", 3) if len(parts) != 2 { return } c.Foreground = Ansi(parts[0]) c.Background = Ansi(parts[1]) } type History []*Set func (c *History) Len() int { return len(*c) } func (c *History) Add(background, foreground Ansi) { colors := &Set{ Foreground: foreground, Background: background, } if c.Len() == 0 { *c = append(*c, colors) return } last := (*c)[c.Len()-1] // never add the same colors twice if last.Foreground == colors.Foreground && last.Background == colors.Background { return } *c = append(*c, colors) } func (c *History) Pop() { if c.Len() == 0 { return } *c = (*c)[:c.Len()-1] } func (c *History) Background() Ansi { if c.Len() == 0 { return emptyColor } return (*c)[c.Len()-1].Background } func (c *History) Foreground() Ansi { if c.Len() == 0 { return emptyColor } return (*c)[c.Len()-1].Foreground } // Ansi is an ANSI color code ready to be printed to the console. // Example: "38;2;255;255;255", "48;2;255;255;255", "31", "95". type Ansi string const ( emptyColor = Ansi("") ) func (c Ansi) IsEmpty() bool { return c == emptyColor } func (c Ansi) IsTransparent() bool { return c == Transparent } func (c Ansi) IsClear() bool { return c == Transparent || c == emptyColor } func (c Ansi) ToForeground() Ansi { colorString := c.String() if strings.HasPrefix(colorString, "38;") { return Ansi(strings.Replace(colorString, "38;", "48;", 1)) } return c } func (c Ansi) ResolveTemplate() Ansi { if c.IsEmpty() { return c } if c.IsTransparent() { return emptyColor } text, err := template.Render(string(c), nil) if err != nil { return Transparent } return Ansi(text) } func (c Ansi) String() string { return string(c) } func MakeColors(palette Palette, cacheEnabled bool, accentColor Ansi, env runtime.Environment) (colors String) { defaultColors := &Defaults{} defaultColors.SetAccentColor(env, accentColor) colors = defaultColors if palette != nil { colors = &PaletteColors{ansiColors: colors, palette: palette} } if cacheEnabled { colors = &Cached{ansiColors: colors} } return } func (d *Defaults) SetAccentColor(env runtime.Environment, defaultColor Ansi) { defer log.Trace(time.Now()) // get accent color from session cache first if accent, OK := cache.Get[*Set](cache.Device, accentColor); OK { d.accent = accent return } rgb, err := GetAccentColor(env) if err != nil { d.accent = &Set{ Foreground: d.ToAnsi(defaultColor, false), Background: d.ToAnsi(defaultColor, true), } return } if defaultColor == "" { return } foreground := color.RGB(rgb.R, rgb.G, rgb.B, false) background := color.RGB(rgb.R, rgb.G, rgb.B, true) d.accent = &Set{ Foreground: Ansi(foreground.String()), Background: Ansi(background.String()), } cache.Set(cache.Device, accentColor, d.accent, cache.INFINITE) } type RGB struct { R, G, B uint8 } // Defaults is the default AnsiColors implementation. type Defaults struct { accent *Set } var ( // Map for color names and their respective foreground [0] or background [1] color codes ansiColorCodes = map[Ansi][2]Ansi{ "black": {"30", "40"}, "red": {"31", "41"}, "green": {"32", "42"}, "yellow": {"33", "43"}, "blue": {"34", "44"}, "magenta": {"35", "45"}, "cyan": {"36", "46"}, "white": {"37", "47"}, "default": {"39", "49"}, "darkGray": {"90", "100"}, "lightRed": {"91", "101"}, "lightGreen": {"92", "102"}, "lightYellow": {"93", "103"}, "lightBlue": {"94", "104"}, "lightMagenta": {"95", "105"}, "lightCyan": {"96", "106"}, "lightWhite": {"97", "107"}, } ) func (d *Defaults) ToAnsi(ansiColor Ansi, isBackground bool) Ansi { if ansiColor == "" { return emptyColor } if ansiColor.IsTransparent() { return ansiColor } if ansiColor == Accent { if d.accent == nil { return emptyColor } if isBackground { return d.accent.Background } return d.accent.Foreground } colorFromName, err := getAnsiColorFromName(ansiColor, isBackground) if err == nil { return colorFromName } colorString := ansiColor.String() if !strings.HasPrefix(colorString, "#") { val, err := strconv.ParseUint(colorString, 10, 64) if err != nil || val > 255 { return emptyColor } c256 := color.C256(uint8(val), isBackground) return Ansi(c256.String()) } style := color.HEX(colorString, isBackground) if !style.IsEmpty() { if TrueColor { return Ansi(style.String()) } return Ansi(style.C256().String()) } if colorInt, err := strconv.ParseInt(colorString, 10, 8); err == nil { c := color.C256(uint8(colorInt), isBackground) return Ansi(c.String()) } return emptyColor } func (d *Defaults) Resolve(colorString Ansi) (Ansi, error) { return colorString, nil } // getAnsiColorFromName returns the color code for a given color name if the name is // known ANSI color name. func getAnsiColorFromName(colorValue Ansi, isBackground bool) (Ansi, error) { if colorCodes, found := ansiColorCodes[colorValue]; found { return colorCodes[generics.ToInt[int](isBackground)], nil } return "", fmt.Errorf("color name %s does not exist", colorValue) } func IsAnsiColorName(colorValue Ansi) bool { _, ok := ansiColorCodes[colorValue] return ok } // PaletteColors is the AnsiColors Decorator that uses the Palette to do named color // lookups before ANSI color code generation. type PaletteColors struct { ansiColors String palette Palette } func (p *PaletteColors) ToAnsi(colorString Ansi, isBackground bool) Ansi { paletteColor, err := p.palette.ResolveColor(colorString) if err != nil { return emptyColor } ansiColor := p.ansiColors.ToAnsi(paletteColor, isBackground) return ansiColor } func (p *PaletteColors) Resolve(colorString Ansi) (Ansi, error) { return p.palette.ResolveColor(colorString) } // Cached is the AnsiColors Decorator that does simple color lookup caching. // ToColor calls are cheap, but not free, and having a simple cache in // has measurable positive effect on performance. type Cached struct { ansiColors String colorCache map[cachedColorKey]Ansi } type cachedColorKey struct { colorString Ansi isBackground bool } func (c *Cached) ToAnsi(colorString Ansi, isBackground bool) Ansi { if c.colorCache == nil { c.colorCache = make(map[cachedColorKey]Ansi) } key := cachedColorKey{colorString, isBackground} if ansiColor, hit := c.colorCache[key]; hit { return ansiColor } ansiColor := c.ansiColors.ToAnsi(colorString, isBackground) c.colorCache[key] = ansiColor return ansiColor } func (c *Cached) Resolve(colorString Ansi) (Ansi, error) { return c.ansiColors.Resolve(colorString) } ================================================ FILE: src/color/colors_darwin.go ================================================ package color import ( "errors" "strconv" "github.com/jandedobbeleer/oh-my-posh/src/log" "github.com/jandedobbeleer/oh-my-posh/src/runtime" ) func GetAccentColor(env runtime.Environment) (*RGB, error) { output, err := env.RunCommand("defaults", "read", "-g", "AppleAccentColor") if err != nil { log.Error(err) return nil, errors.New("unable to read accent color") } index, err := strconv.Atoi(output) if err != nil { log.Error(err) return nil, errors.New("unable to parse accent color index") } var accentColors = map[int]RGB{ -1: {152, 152, 152}, // Graphite 0: {224, 55, 62}, // Red 1: {247, 130, 25}, // Orange 2: {255, 199, 38}, // Yellow 3: {96, 186, 70}, // Green 4: {0, 122, 255}, // Blue 5: {149, 61, 150}, // Purple 6: {247, 79, 159}, // Pink } color, exists := accentColors[index] if !exists { color = accentColors[6] // Default to graphite (white) } return &color, nil } ================================================ FILE: src/color/colors_test.go ================================================ package color import ( "errors" "testing" "github.com/alecthomas/assert" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/runtime/mock" "github.com/jandedobbeleer/oh-my-posh/src/template" ) func TestGetAnsiFromColorString(t *testing.T) { cases := []struct { Case string Expected Ansi Color Ansi Background bool Color256 bool }{ {Case: "256 color", Expected: Ansi("38;5;99"), Color: "99", Background: false}, {Case: "256 color", Expected: Ansi("38;5;122"), Color: "122", Background: false}, {Case: "Invalid background", Expected: emptyColor, Color: "invalid", Background: true}, {Case: "Invalid background", Expected: emptyColor, Color: "invalid", Background: false}, {Case: "Hex foreground", Expected: Ansi("38;2;170;187;204"), Color: "#AABBCC", Background: false}, {Case: "Hex background", Expected: Ansi("48;2;170;187;204"), Color: "#AABBCC", Background: true}, {Case: "Base 8 foreground", Expected: Ansi("31"), Color: "red", Background: false}, {Case: "Base 8 background", Expected: Ansi("41"), Color: "red", Background: true}, {Case: "Base 16 foreground", Expected: Ansi("91"), Color: "lightRed", Background: false}, {Case: "Base 16 background", Expected: Ansi("101"), Color: "lightRed", Background: true}, {Case: "Non true color TERM", Expected: Ansi("38;5;146"), Color: "#AABBCC", Color256: true}, } for _, tc := range cases { ansiColors := &Defaults{} TrueColor = !tc.Color256 ansiColor := ansiColors.ToAnsi(tc.Color, tc.Background) assert.Equal(t, tc.Expected, ansiColor, tc.Case) } } func TestMakeColors(t *testing.T) { env := &mock.Environment{} cache.Set(cache.Device, accentColor, &Set{}, cache.INFINITE) defer cache.DeleteAll(cache.Device) env.On("WindowsRegistryKeyValue", `HKEY_CURRENT_USER\Software\Microsoft\Windows\DWM\ColorizationColor`).Return(&runtime.WindowsRegistryValue{}, errors.New("err")) colors := MakeColors(nil, false, "", env) assert.IsType(t, &Defaults{}, colors) colors = MakeColors(nil, true, "", env) assert.IsType(t, &Cached{}, colors) assert.IsType(t, &Defaults{}, colors.(*Cached).ansiColors) colors = MakeColors(testPalette, false, "", env) assert.IsType(t, &PaletteColors{}, colors) assert.IsType(t, &Defaults{}, colors.(*PaletteColors).ansiColors) colors = MakeColors(testPalette, true, "", env) assert.IsType(t, &Cached{}, colors) assert.IsType(t, &PaletteColors{}, colors.(*Cached).ansiColors) assert.IsType(t, &Defaults{}, colors.(*Cached).ansiColors.(*PaletteColors).ansiColors) } func TestAnsiRender(t *testing.T) { cases := []struct { Case string Expected Ansi Term string }{ {Case: "Inside vscode", Expected: "#123456", Term: "vscode"}, {Case: "Outside vscode", Expected: "", Term: "windowsterminal"}, } for _, tc := range cases { env := new(mock.Environment) env.On("Getenv", "TERM_PROGRAM").Return(tc.Term) env.On("Shell").Return("foo") template.Cache = new(cache.Template) template.Init(env, nil, nil) ansi := Ansi("{{ if eq \"vscode\" .Env.TERM_PROGRAM }}#123456{{end}}") got := ansi.ResolveTemplate() assert.Equal(t, tc.Expected, got, tc.Case) } } ================================================ FILE: src/color/colors_unix.go ================================================ //go:build !windows && !darwin package color import "github.com/jandedobbeleer/oh-my-posh/src/runtime" func GetAccentColor(_ runtime.Environment) (*RGB, error) { return nil, &runtime.NotImplemented{} } ================================================ FILE: src/color/colors_windows.go ================================================ package color import ( "errors" "time" "github.com/jandedobbeleer/oh-my-posh/src/log" "github.com/jandedobbeleer/oh-my-posh/src/runtime" ) func GetAccentColor(env runtime.Environment) (*RGB, error) { defer log.Trace(time.Now()) if env == nil { return nil, errors.New("unable to get color without environment") } // see https://stackoverflow.com/questions/3560890/vista-7-how-to-get-glass-color value, err := env.WindowsRegistryKeyValue(`HKEY_CURRENT_USER\Software\Microsoft\Windows\DWM\ColorizationColor`) if err != nil || value.ValueType != runtime.DWORD { return nil, err } return &RGB{ R: byte(value.DWord >> 16), G: byte(value.DWord >> 8), B: byte(value.DWord), }, nil } ================================================ FILE: src/color/cycle.go ================================================ package color type Cycle []*Set func (c Cycle) Loop() (*Set, Cycle) { if len(c) == 0 { return nil, c } return c[0], append(c[1:], c[0]) } ================================================ FILE: src/color/keywords.go ================================================ package color const ( // Transparent implies a transparent color Transparent Ansi = "transparent" // Accent is the OS accent color Accent Ansi = "accent" // ParentBackground takes the previous segment's background color ParentBackground Ansi = "parentBackground" // ParentForeground takes the previous segment's color ParentForeground Ansi = "parentForeground" // Background takes the current segment's background color Background Ansi = "background" // Foreground takes the current segment's foreground color Foreground Ansi = "foreground" ) func (color Ansi) isKeyword() bool { switch color { //nolint: exhaustive case Transparent, ParentBackground, ParentForeground, Background, Foreground: return true default: return false } } func (color Ansi) Resolve(current *Set, parents []*Set) Ansi { resolveParentColor := func(keyword Ansi) Ansi { for _, parentColor := range parents { if parentColor == nil { return Transparent } switch keyword { //nolint: exhaustive case ParentBackground: keyword = parentColor.Background case ParentForeground: keyword = parentColor.Foreground default: if keyword == "" { return Transparent } return keyword } } if keyword == "" { return Transparent } return keyword } resolveKeyword := func(keyword Ansi) Ansi { switch { case keyword == Background && current != nil: return current.Background case keyword == Foreground && current != nil: return current.Foreground case (keyword == ParentBackground || keyword == ParentForeground) && parents != nil: return resolveParentColor(keyword) default: return Transparent } } for color.isKeyword() { resolved := resolveKeyword(color) if resolved == color { break } color = resolved } return color } ================================================ FILE: src/color/palette.go ================================================ package color import ( "fmt" "sort" "strings" ) type Palette map[Ansi]Ansi const ( paletteKeyPrefix = "p:" paletteKeyError = "palette: requested color %s does not exist in palette of colors %s" paletteMaxRecursionDepth = 3 // allows 3 or less recursive resolutions paletteRecursiveKeyError = "palette: recursive resolution of color %s returned palette reference %s and reached recursion depth %d" ) // ResolveColor gets a color value from the palette using given colorName. // If colorName is not a palette reference, it is returned as is. func (p Palette) ResolveColor(colorName Ansi) (Ansi, error) { return p.resolveColor(colorName, 1, &colorName) } // originalColorName is a pointer to save allocations func (p Palette) resolveColor(colorName Ansi, depth int, originalColorName *Ansi) (Ansi, error) { key, ok := asPaletteKey(colorName) // colorName is not a palette key, return it as is if !ok { return colorName, nil } color, ok := p[key] if !ok { return "", &PaletteKeyError{Key: key, palette: p} } if _, isKey := isPaletteKey(color); isKey { if depth > paletteMaxRecursionDepth { return "", &PaletteRecursiveKeyError{Key: *originalColorName, Value: color, depth: depth} } return p.resolveColor(color, depth+1, originalColorName) } return color, nil } func asPaletteKey(colorName Ansi) (Ansi, bool) { prefix, isKey := isPaletteKey(colorName) if !isKey { return "", false } key := strings.TrimPrefix(colorName.String(), prefix.String()) return Ansi(key), true } func isPaletteKey(colorName Ansi) (Ansi, bool) { return paletteKeyPrefix, strings.HasPrefix(colorName.String(), paletteKeyPrefix) } // PaletteKeyError records the missing Palette key. type PaletteKeyError struct { palette Palette Key Ansi } func (p *PaletteKeyError) Error() string { keys := make([]string, 0, len(p.palette)) for key := range p.palette { keys = append(keys, key.String()) } sort.Strings(keys) allColors := strings.Join(keys, ",") errorStr := fmt.Sprintf(paletteKeyError, p.Key, allColors) return errorStr } // PaletteRecursiveKeyError records the Palette key and resolved color value (which // is also a Palette key) type PaletteRecursiveKeyError struct { Key Ansi Value Ansi depth int } func (p *PaletteRecursiveKeyError) Error() string { errorStr := fmt.Sprintf(paletteRecursiveKeyError, p.Key, p.Value, p.depth) return errorStr } // MaybeResolveColor wraps resolveColor and silences possible errors, returning // Transparent color by default, as a Block does not know how to handle color errors. func (p Palette) MaybeResolveColor(colorName Ansi) Ansi { color, err := p.ResolveColor(colorName) if err != nil { return "" } return color } ================================================ FILE: src/color/palette_test.go ================================================ package color import ( "testing" "github.com/alecthomas/assert" ) var ( testPalette = Palette{ "red": "#FF0000", "green": "#00FF00", "blue": "#0000FF", "white": "#FFFFFF", "black": "#000000", } ) type TestPaletteRequest struct { Case string Request Ansi Expected Ansi ExpectedError bool } func TestPaletteShouldResolveColorFromTestPalette(t *testing.T) { cases := []TestPaletteRequest{ {Case: "Palette red", Request: "p:red", Expected: "#FF0000"}, {Case: "Palette green", Request: "p:green", Expected: "#00FF00"}, {Case: "Palette blue", Request: "p:blue", Expected: "#0000FF"}, {Case: "Palette white", Request: "p:white", Expected: "#FFFFFF"}, {Case: "Palette black", Request: "p:black", Expected: "#000000"}, } for _, tc := range cases { testPaletteRequest(t, tc) } } func testPaletteRequest(t *testing.T, tc TestPaletteRequest) { actual, err := testPalette.ResolveColor(tc.Request) if !tc.ExpectedError { assert.Nil(t, err, tc.Case) assert.Equal(t, tc.Expected, actual, "expected different color value") } else { assert.NotNil(t, err, tc.Case) assert.Equal(t, string(tc.Expected), err.Error()) } } func TestPaletteShouldIgnoreNonPaletteColors(t *testing.T) { cases := []TestPaletteRequest{ {Case: "Deep puprple", Request: "#1F1137", Expected: "#1F1137"}, {Case: "Light red", Request: "#D55252", Expected: "#D55252"}, {Case: "ANSI black", Request: "black", Expected: "black"}, {Case: "Foreground", Request: "foreground", Expected: "foreground"}, } for _, tc := range cases { testPaletteRequest(t, tc) } } func TestPaletteShouldReturnErrorOnMissingColor(t *testing.T) { cases := []TestPaletteRequest{ { Case: "Palette deep purple", Request: "p:deep-purple", ExpectedError: true, Expected: "palette: requested color deep-purple does not exist in palette of colors black,blue,green,red,white", }, { Case: "Palette cyan", Request: "p:cyan", ExpectedError: true, Expected: "palette: requested color cyan does not exist in palette of colors black,blue,green,red,white", }, { Case: "Palette foreground", Request: "p:foreground", ExpectedError: true, Expected: "palette: requested color foreground does not exist in palette of colors black,blue,green,red,white", }, } for _, tc := range cases { testPaletteRequest(t, tc) } } func TestPaletteShouldHandleMixedCases(t *testing.T) { cases := []TestPaletteRequest{ {Case: "Palette red", Request: "p:red", Expected: "#FF0000"}, {Case: "ANSI black", Request: "black", Expected: "black"}, {Case: "Cyan", Request: "#05E6FA", Expected: "#05E6FA"}, {Case: "Palette black", Request: "p:black", Expected: "#000000"}, {Case: "Palette pink", Request: "p:pink", ExpectedError: true, Expected: "palette: requested color pink does not exist in palette of colors black,blue,green,red,white"}, } for _, tc := range cases { testPaletteRequest(t, tc) } } func TestPaletteShouldUseEmptyColorByDefault(t *testing.T) { cases := []TestPaletteRequest{ {Case: "Palette magenta", Request: "p:magenta", Expected: ""}, {Case: "Palette gray", Request: "p:gray", Expected: ""}, {Case: "Palette rose", Request: "p:rose", Expected: ""}, } for _, tc := range cases { actual := testPalette.MaybeResolveColor(tc.Request) assert.Equal(t, tc.Expected, actual, "expected different color value") } } func TestPaletteShouldResolveRecursiveReference(t *testing.T) { tp := Palette{ "light-blue": "#CAF0F8", "dark-blue": "#023E8A", "foreground": "p:light-blue", "background": "p:dark-blue", "text": "p:foreground", "icon": "p:background", "void": "p:void", // infinite recursion - error "1": "white", "2": "p:1", "3": "p:2", "4": "p:3", // 3 recursive lookups - allowed "5": "p:4", // 4 recursive lookups - error } cases := []TestPaletteRequest{ { Case: "Palette light-blue", Request: "p:light-blue", Expected: "#CAF0F8", }, { Case: "Palette foreground", Request: "p:foreground", Expected: "#CAF0F8", }, { Case: "Palette background", Request: "p:background", Expected: "#023E8A", }, { Case: "Palette text (2 recursive lookups)", Request: "p:text", Expected: "#CAF0F8", }, { Case: "Palette icon (2 recursive lookups)", Request: "p:icon", Expected: "#023E8A", }, { Case: "Palette void (infinite recursion)", Request: "p:void", ExpectedError: true, Expected: "palette: recursive resolution of color p:void returned palette reference p:void and reached recursion depth 4", }, { Case: "Palette p:4 (3 recursive lookups)", Request: "p:4", Expected: "white", }, { Case: "Palette p:5 (4 recursive lookups)", Request: "p:5", ExpectedError: true, Expected: "palette: recursive resolution of color p:5 returned palette reference p:1 and reached recursion depth 4", }, } for _, tc := range cases { actual, err := tp.ResolveColor(tc.Request) if !tc.ExpectedError { assert.Nil(t, err, "expected no error") assert.Equal(t, tc.Expected, actual, "expected different color value") } else { assert.NotNil(t, err, "expected error") assert.Equal(t, string(tc.Expected), err.Error()) } } } func TestPaletteShouldHandleEmptyKey(t *testing.T) { tp := Palette{ "": "#000000", } actual, err := tp.ResolveColor("p:") assert.Nil(t, err, "expected no error") assert.Equal(t, Ansi("#000000"), actual, "expected different color value") } func BenchmarkPaletteMixedCaseResolution(b *testing.B) { for b.Loop() { benchmarkPaletteMixedCaseResolution() } } func benchmarkPaletteMixedCaseResolution() { cases := []TestPaletteRequest{ {Case: "Palette red", Request: "p:red", Expected: "#FF0000"}, {Case: "ANSI black", Request: "black", Expected: "black"}, {Case: "Cyan", Request: "#05E6FA", Expected: "#05E6FA"}, {Case: "Palette black", Request: "p:black", Expected: "#000000"}, {Case: "Palette pink", Request: "p:pink", ExpectedError: true, Expected: "palette: requested color pink does not exist in palette of colors black,blue,green,red,white"}, {Case: "Palette blue", Request: "p:blue", Expected: "#0000FF"}, // repeating the same set to have longer benchmarks {Case: "Palette red", Request: "p:red", Expected: "#FF0000"}, {Case: "ANSI black", Request: "black", Expected: "black"}, {Case: "Cyan", Request: "#05E6FA", Expected: "#05E6FA"}, {Case: "Palette black", Request: "p:black", Expected: "#000000"}, {Case: "Palette pink", Request: "p:pink", ExpectedError: true, Expected: "palette: requested color pink does not exist in palette of colors black,blue,green,red,white"}, {Case: "Palette blue", Request: "p:blue", Expected: "#0000FF"}, } for _, tc := range cases { // both value and error values are irrelevant, but such assignment calms down // golangci-lint "return value of `testPalette.ResolveColor` is not checked" error _, _ = testPalette.ResolveColor(tc.Request) } } ================================================ FILE: src/color/palettes.go ================================================ package color type Palettes struct { List map[string]Palette `json:"list,omitempty" toml:"list,omitempty" yaml:"list,omitempty"` Template string `json:"template,omitempty" toml:"template,omitempty" yaml:"template,omitempty"` } ================================================ FILE: src/config/backup.go ================================================ package config import ( "bytes" "encoding/json" "io" "os" "strings" toml "github.com/pelletier/go-toml/v2" yaml "go.yaml.in/yaml/v3" ) func (cfg *Config) Backup() { dst := cfg.Source + ".bak" source, err := os.Open(cfg.Source) if err != nil { return } defer source.Close() destination, err := os.Create(dst) if err != nil { return } defer destination.Close() _, err = io.Copy(destination, source) if err != nil { return } } func (cfg *Config) Export(format string) string { if len(format) != 0 { cfg.Format = format } var result bytes.Buffer switch cfg.Format { case YAML: prefix := "# yaml-language-server: $schema=https://raw.githubusercontent.com/JanDeDobbeleer/oh-my-posh/main/themes/schema.json\n\n" yamlEncoder := yaml.NewEncoder(&result) err := yamlEncoder.Encode(cfg) if err != nil { return "" } return prefix + result.String() case JSON: jsonEncoder := json.NewEncoder(&result) jsonEncoder.SetEscapeHTML(false) jsonEncoder.SetIndent("", " ") _ = jsonEncoder.Encode(cfg) prefix := "{\n \"$schema\": \"https://raw.githubusercontent.com/JanDeDobbeleer/oh-my-posh/main/themes/schema.json\"," data := strings.Replace(result.String(), "{", prefix, 1) return EscapeGlyphs(data, cfg.MigrateGlyphs) case TOML: tomlEncoder := toml.NewEncoder(&result) tomlEncoder.SetIndentTables(true) err := tomlEncoder.Encode(cfg) if err != nil { return "" } return result.String() } // unsupported format return "" } func (cfg *Config) Write(format string) { content := cfg.Export(format) if content == "" { // we are unable to perform the export return } f, err := os.OpenFile(cfg.Source, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { return } defer func() { _ = f.Close() }() _, err = f.WriteString(content) if err != nil { return } } ================================================ FILE: src/config/block.go ================================================ package config import "fmt" // BlockType type of block type BlockType string // BlockAlignment alignment of a Block type BlockAlignment string // Overflow defines how to handle a right block that overflows with the previous block type Overflow string const ( // Prompt writes one or more Segments Prompt BlockType = "prompt" // RPrompt is a right aligned prompt RPrompt BlockType = "rprompt" // Left aligns left Left BlockAlignment = "left" // Right aligns right Right BlockAlignment = "right" // Break adds a line break Break Overflow = "break" // Hide hides the block Hide Overflow = "hide" ) // Block defines a part of the prompt with optional segments type Block struct { Type BlockType `json:"type,omitempty" toml:"type,omitempty" yaml:"type,omitempty"` Alignment BlockAlignment `json:"alignment,omitempty" toml:"alignment,omitempty" yaml:"alignment,omitempty"` Filler string `json:"filler,omitempty" toml:"filler,omitempty" yaml:"filler,omitempty"` Overflow Overflow `json:"overflow,omitempty" toml:"overflow,omitempty" yaml:"overflow,omitempty"` LeadingDiamond string `json:"leading_diamond,omitempty" toml:"leading_diamond,omitempty" yaml:"leading_diamond,omitempty"` TrailingDiamond string `json:"trailing_diamond,omitempty" toml:"trailing_diamond,omitempty" yaml:"trailing_diamond,omitempty"` Segments []*Segment `json:"segments,omitempty" toml:"segments,omitempty" yaml:"segments,omitempty"` Newline bool `json:"newline,omitempty" toml:"newline,omitempty" yaml:"newline,omitempty"` Force bool `json:"force,omitempty" toml:"force,omitempty" yaml:"force,omitempty"` Index int `json:"index,omitempty" toml:"index,omitempty" yaml:"index,omitempty"` } func (b *Block) key() any { if b.Index > 0 { return b.Index - 1 } return fmt.Sprintf("%s-%s", b.Type, b.Alignment) } ================================================ FILE: src/config/cache.go ================================================ package config import "github.com/jandedobbeleer/oh-my-posh/src/cache" type Cache struct { Duration cache.Duration `json:"duration,omitempty" toml:"duration,omitempty" yaml:"duration,omitempty"` Strategy Strategy `json:"strategy,omitempty" toml:"strategy,omitempty" yaml:"strategy,omitempty"` } type Strategy string const ( Folder Strategy = "folder" Session Strategy = "session" Device Strategy = "device" ) ================================================ FILE: src/config/config.go ================================================ package config import ( "encoding/gob" "slices" "strings" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/cli/upgrade" "github.com/jandedobbeleer/oh-my-posh/src/color" "github.com/jandedobbeleer/oh-my-posh/src/log" "github.com/jandedobbeleer/oh-my-posh/src/maps" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/segments" "github.com/jandedobbeleer/oh-my-posh/src/shell" "github.com/jandedobbeleer/oh-my-posh/src/template" "github.com/jandedobbeleer/oh-my-posh/src/terminal" ) func init() { gob.Register(&Config{}) } const ( JSON string = "json" YAML string = "yaml" TOML string = "toml" TML string = "tml" YML string = "yml" JSONC string = "jsonc" AUTOUPGRADE = "upgrade" UPGRADENOTICE = "notice" RELOAD = "reload" Version = 4 ) type Action string func (a Action) IsDefault() bool { return a != Prepend && a != Extend } const ( Prepend Action = "prepend" Extend Action = "extend" ) // Config holds all the theme for rendering the prompt type Config struct { Palette color.Palette `json:"palette,omitempty" toml:"palette,omitempty" yaml:"palette,omitempty"` DebugPrompt *Segment `json:"debug_prompt,omitempty" toml:"debug_prompt,omitempty" yaml:"debug_prompt,omitempty"` Var map[string]any `json:"var,omitempty" toml:"var,omitempty" yaml:"var,omitempty"` Palettes *color.Palettes `json:"palettes,omitempty" toml:"palettes,omitempty" yaml:"palettes,omitempty"` ValidLine *Segment `json:"valid_line,omitempty" toml:"valid_line,omitempty" yaml:"valid_line,omitempty"` SecondaryPrompt *Segment `json:"secondary_prompt,omitempty" toml:"secondary_prompt,omitempty" yaml:"secondary_prompt,omitempty"` TransientPrompt *Segment `json:"transient_prompt,omitempty" toml:"transient_prompt,omitempty" yaml:"transient_prompt,omitempty"` ErrorLine *Segment `json:"error_line,omitempty" toml:"error_line,omitempty" yaml:"error_line,omitempty"` Maps *maps.Config `json:"maps,omitempty" toml:"maps,omitempty" yaml:"maps,omitempty"` Upgrade *upgrade.Config `json:"upgrade,omitempty" toml:"upgrade,omitempty" yaml:"upgrade,omitempty"` Extends string `json:"extends,omitempty" toml:"extends,omitempty" yaml:"extends,omitempty"` AccentColor color.Ansi `json:"accent_color,omitempty" toml:"accent_color,omitempty" yaml:"accent_color,omitempty"` ConsoleTitleTemplate string `json:"console_title_template,omitempty" toml:"console_title_template,omitempty" yaml:"console_title_template,omitempty"` PWD string `json:"pwd,omitempty" toml:"pwd,omitempty" yaml:"pwd,omitempty"` Source string `json:"-" toml:"-" yaml:"-"` Format string `json:"-" toml:"-" yaml:"-"` TerminalBackground color.Ansi `json:"terminal_background,omitempty" toml:"terminal_background,omitempty" yaml:"terminal_background,omitempty"` ToolTipsAction Action `json:"tooltips_action,omitempty" toml:"tooltips_action,omitempty" yaml:"tooltips_action,omitempty"` Blocks []*Block `json:"blocks,omitempty" toml:"blocks,omitempty" yaml:"blocks,omitempty"` Cycle color.Cycle `json:"cycle,omitempty" toml:"cycle,omitempty" yaml:"cycle,omitempty"` ITermFeatures terminal.ITermFeatures `json:"iterm_features,omitempty" toml:"iterm_features,omitempty" yaml:"iterm_features,omitempty"` Tooltips []*Segment `json:"tooltips,omitempty" toml:"tooltips,omitempty" yaml:"tooltips,omitempty"` hash uint64 Version int `json:"version" toml:"version" yaml:"version"` MigrateGlyphs bool `json:"-" toml:"-" yaml:"-"` Async bool `json:"async,omitempty" toml:"async,omitempty" yaml:"async,omitempty"` ShellIntegration bool `json:"shell_integration,omitempty" toml:"shell_integration,omitempty" yaml:"shell_integration,omitempty"` FinalSpace bool `json:"final_space,omitempty" toml:"final_space,omitempty" yaml:"final_space,omitempty"` UpgradeNotice bool `json:"-" toml:"-" yaml:"-"` extended bool PatchPwshBleed bool `json:"patch_pwsh_bleed,omitempty" toml:"patch_pwsh_bleed,omitempty" yaml:"patch_pwsh_bleed,omitempty"` AutoUpgrade bool `json:"-" toml:"-" yaml:"-"` EnableCursorPositioning bool `json:"enable_cursor_positioning,omitempty" toml:"enable_cursor_positioning,omitempty" yaml:"enable_cursor_positioning,omitempty"` Streaming int `json:"streaming,omitempty" toml:"streaming,omitempty" yaml:"streaming,omitempty"` } func (cfg *Config) MakeColors(env runtime.Environment) color.String { cacheDisabled := env.Getenv("OMP_CACHE_DISABLED") == "1" return color.MakeColors(cfg.getPalette(), !cacheDisabled, cfg.AccentColor, env) } func (cfg *Config) getPalette() color.Palette { if cfg.Palettes == nil { return cfg.Palette } key, err := template.Render(cfg.Palettes.Template, nil) if err != nil { return cfg.Palette } palette, ok := cfg.Palettes.List[key] if !ok { return cfg.Palette } for key, color := range cfg.Palette { if _, ok := palette[key]; ok { continue } palette[key] = color } return palette } func (cfg *Config) Features(env runtime.Environment) shell.Features { var feats shell.Features asyncShells := []string{shell.BASH, shell.ZSH, shell.FISH, shell.PWSH} if cfg.Async && slices.Contains(asyncShells, env.Shell()) { log.Debug("async enabled") feats |= shell.Async } if cfg.TransientPrompt != nil { log.Debug("transient prompt enabled") feats |= shell.Transient } if cfg.Streaming > 0 { log.Debug("streaming enabled") feats |= shell.Streaming } if feats&(shell.Streaming|shell.Transient) != 0 { feats |= shell.KeyHandlers } unsupportedShells := []string{shell.ELVISH, shell.XONSH} if slices.Contains(unsupportedShells, env.Shell()) { cfg.ShellIntegration = false } if cfg.ShellIntegration { log.Debug("shell integration enabled") feats |= shell.FTCSMarks } // do not enable upgrade features when async is enabled if feats&shell.Async == 0 { feats |= cfg.upgradeFeatures() } if cfg.ErrorLine != nil || cfg.ValidLine != nil { log.Debug("error or valid line enabled") feats |= shell.LineError } if len(cfg.Tooltips) > 0 { log.Debug("tooltips enabled") feats |= shell.Tooltips } if env.Shell() == shell.FISH && cfg.ITermFeatures != nil && cfg.ITermFeatures.Contains(terminal.PromptMark) { log.Debug("prompt mark enabled") feats |= shell.PromptMark } for i, block := range cfg.Blocks { if (i == 0 && block.Newline) && cfg.EnableCursorPositioning { log.Debug("cursor positioning enabled") feats |= shell.CursorPositioning } if block.Type == RPrompt { log.Debug("rprompt enabled") feats |= shell.RPrompt } for _, segment := range block.Segments { if segment.Type == AZ { source := segment.Options.String(segments.Source, segments.FirstMatch) if strings.Contains(source, segments.Pwsh) { log.Debug("azure enabled") feats |= shell.Azure } } if segment.Type == GIT { source := segment.Options.String(segments.Source, segments.Cli) if source == segments.Pwsh { log.Debug("posh-git enabled") feats |= shell.PoshGit } } } } return feats } func (cfg *Config) upgradeFeatures() shell.Features { var feats shell.Features autoUpgrade := cfg.Upgrade.Auto if val, OK := cache.Get[bool](cache.Device, AUTOUPGRADE); OK { log.Debug("auto upgrade key found, overriding config") autoUpgrade = val } upgradeNotice := cfg.Upgrade.DisplayNotice if val, OK := cache.Get[bool](cache.Device, UPGRADENOTICE); OK { log.Debug("upgrade notice key found, overriding config") upgradeNotice = val } if upgradeNotice && !autoUpgrade { log.Debug("notice enabled, no auto upgrade") feats |= shell.Notice } if autoUpgrade { log.Debug("auto upgrade enabled") feats |= shell.Upgrade } return feats } func (cfg *Config) Hash() uint64 { return cfg.hash } // migrateSegmentProperties migrates the deprecated Properties field to Options for all segments. // This is needed for TOML configs since go-toml/v2 doesn't support custom unmarshalers. func (cfg *Config) migrateSegmentProperties() { for _, block := range cfg.Blocks { for _, segment := range block.Segments { segment.MigratePropertiesToOptions() } } } // toggleSegments processes all segments in all blocks and adds segments // with Toggled == true to the toggle cache, effectively toggling them off. func (cfg *Config) toggleSegments() { currentToggleSet, _ := cache.Get[map[string]bool](cache.Session, cache.TOGGLECACHE) if currentToggleSet == nil { currentToggleSet = make(map[string]bool) } for _, block := range cfg.Blocks { for _, segment := range block.Segments { if segment.Toggled { segmentName := segment.Alias if segmentName == "" { segmentName = string(segment.Type) } currentToggleSet[segmentName] = true } } } // Update cache with the map directly cache.Set(cache.Session, cache.TOGGLECACHE, currentToggleSet, cache.INFINITE) } ================================================ FILE: src/config/config_test.go ================================================ package config import ( "testing" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/cli/upgrade" "github.com/jandedobbeleer/oh-my-posh/src/color" "github.com/jandedobbeleer/oh-my-posh/src/runtime/mock" "github.com/jandedobbeleer/oh-my-posh/src/shell" "github.com/jandedobbeleer/oh-my-posh/src/template" "github.com/stretchr/testify/assert" ) func TestGetPalette(t *testing.T) { palette := color.Palette{ "red": "#ff0000", "blue": "#0000ff", } cases := []struct { Palettes *color.Palettes Palette color.Palette ExpectedPalette color.Palette Case string }{ { Case: "match", Palettes: &color.Palettes{ Template: "{{ .Shell }}", List: map[string]color.Palette{ "bash": palette, "zsh": { "red": "#ff0001", "blue": "#0000fb", }, }, }, ExpectedPalette: palette, }, { Case: "no match, no fallback", Palettes: &color.Palettes{ Template: "{{ .Shell }}", List: map[string]color.Palette{ "fish": palette, "zsh": { "red": "#ff0001", "blue": "#0000fb", }, }, }, ExpectedPalette: nil, }, { Case: "no match, default", Palettes: &color.Palettes{ Template: "{{ .Shell }}", List: map[string]color.Palette{ "zsh": { "red": "#ff0001", "blue": "#0000fb", }, }, }, Palette: palette, ExpectedPalette: palette, }, { Case: "no palettes", ExpectedPalette: nil, }, { Case: "match, with override", Palettes: &color.Palettes{ Template: "{{ .Shell }}", List: map[string]color.Palette{ "bash": { "red": "#ff0001", "yellow": "#ffff00", }, }, }, Palette: palette, ExpectedPalette: color.Palette{ "red": "#ff0001", "blue": "#0000ff", "yellow": "#ffff00", }, }, } for _, tc := range cases { env := &mock.Environment{} env.On("Shell").Return("bash") template.Cache = &cache.Template{ SimpleTemplate: cache.SimpleTemplate{ Shell: "bash", }, } template.Init(env, nil, nil) cfg := &Config{ Palette: tc.Palette, Palettes: tc.Palettes, } got := cfg.getPalette() assert.Equal(t, tc.ExpectedPalette, got, tc.Case) } } func TestUpgradeFeatures(t *testing.T) { cases := []struct { Case string ExpectedFeats shell.Features UpgradeCacheKeyExists bool AutoUpgrade bool Force bool DisplayNotice bool AutoUpgradeKey bool NoticeKey bool }{ { Case: "cache exists, no force", UpgradeCacheKeyExists: true, ExpectedFeats: 0, }, { Case: "auto upgrade enabled", AutoUpgrade: true, ExpectedFeats: shell.Upgrade, }, { Case: "auto upgrade via cache", AutoUpgradeKey: true, ExpectedFeats: shell.Upgrade, }, { Case: "notice enabled, no auto upgrade", DisplayNotice: true, ExpectedFeats: shell.Notice, }, { Case: "notice via cache, no auto upgrade", NoticeKey: true, ExpectedFeats: shell.Notice, }, { Case: "force upgrade ignores cache", UpgradeCacheKeyExists: true, Force: true, AutoUpgrade: true, ExpectedFeats: shell.Upgrade, }, } for _, tc := range cases { if tc.UpgradeCacheKeyExists { cache.Set(cache.Device, upgrade.CACHEKEY, "", cache.INFINITE) } if tc.AutoUpgradeKey { cache.Set(cache.Device, AUTOUPGRADE, true, cache.INFINITE) } if tc.NoticeKey { cache.Set(cache.Device, UPGRADENOTICE, true, cache.INFINITE) } cfg := &Config{ Upgrade: &upgrade.Config{ Auto: tc.AutoUpgrade, Force: tc.Force, DisplayNotice: tc.DisplayNotice, }, } got := cfg.upgradeFeatures() assert.Equal(t, tc.ExpectedFeats, got, tc.Case) cache.DeleteAll(cache.Device) } } ================================================ FILE: src/config/default.go ================================================ package config import ( "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/cli/upgrade" "github.com/jandedobbeleer/oh-my-posh/src/color" "github.com/jandedobbeleer/oh-my-posh/src/segments" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" ) func Default(configError error) *Config { exitBackgroundTemplate := "{{ if gt .Code 0 }}p:red{{ end }}" exitTemplate := " {{ if gt .Code 0 }}\uf00d{{ else }}\uf00c{{ end }} " if configError != nil && configError != ErrNoConfig { exitBackgroundTemplate = "p:red" exitTemplate = configError.Error() } cfg := &Config{ hash: 1234567890, // placeholder hash value Version: 4, FinalSpace: true, Blocks: []*Block{ { Type: Prompt, Alignment: Left, Segments: []*Segment{ { Type: SESSION, Style: Diamond, LeadingDiamond: "\ue0b6", TrailingDiamond: "\ue0b0", Foreground: "p:black", Background: "p:yellow", Template: " {{ if .SSHSession }}\ueba9 {{ end }}{{ .UserName }} ", }, { Type: PATH, Style: Powerline, PowerlineSymbol: "\ue0b0", Foreground: "p:white", Background: "p:orange", Options: options.Map{ options.Style: "folder", }, Template: " \uea83 {{ path .Path .Location }} ", }, { Type: GIT, Style: Powerline, PowerlineSymbol: "\ue0b0", Foreground: "p:black", Background: "p:green", BackgroundTemplates: []string{ "{{ if or (.Working.Changed) (.Staging.Changed) }}p:yellow{{ end }}", "{{ if and (gt .Ahead 0) (gt .Behind 0) }}p:red{{ end }}", "{{ if gt .Ahead 0 }}#49416D{{ end }}", "{{ if gt .Behind 0 }}#7A306C{{ end }}", }, ForegroundTemplates: []string{ "{{ if or (.Working.Changed) (.Staging.Changed) }}p:black{{ end }}", "{{ if and (gt .Ahead 0) (gt .Behind 0) }}p:white{{ end }}", "{{ if gt .Ahead 0 }}p:white{{ end }}", }, Options: options.Map{ segments.BranchTemplate: "{{ trunc 25 .Branch }}", segments.FetchStatus: true, segments.FetchUpstreamIcon: true, }, Template: " {{ if .UpstreamURL }}{{ url .UpstreamIcon .UpstreamURL }} {{ end }}{{ .HEAD }}{{if .BranchStatus }} {{ .BranchStatus }}{{ end }}{{ if .Working.Changed }} \uf044 {{ .Working.String }}{{ end }}{{ if .Staging.Changed }} \uf046 {{ .Staging.String }}{{ end }} ", //nolint:lll }, { Type: ROOT, Style: Powerline, PowerlineSymbol: "\ue0b0", Foreground: "p:white", Background: "p:yellow", Template: " \uf0e7 ", }, { Type: STATUS, Style: Diamond, LeadingDiamond: "\ue0b0", TrailingDiamond: "\ue0b4", Foreground: "p:white", Background: "p:blue", BackgroundTemplates: []string{ exitBackgroundTemplate, }, Options: options.Map{ options.AlwaysEnabled: true, }, Template: exitTemplate, }, }, }, { Type: RPrompt, Segments: []*Segment{ { Type: NODE, Style: Plain, Foreground: "p:green", Background: "transparent", Template: "\ue718 ", Options: options.Map{ segments.HomeEnabled: false, segments.FetchPackageManager: false, segments.DisplayMode: "files", }, }, { Type: GOLANG, Style: Plain, Foreground: "p:blue", Background: "transparent", Template: "\ue626 ", Options: options.Map{ options.FetchVersion: false, }, }, { Type: PYTHON, Style: Plain, Foreground: "p:yellow", Background: "transparent", Template: "\ue235 ", Options: options.Map{ options.FetchVersion: false, segments.DisplayMode: "files", segments.FetchVirtualEnv: false, }, }, { Type: SHELL, Style: Plain, Foreground: "p:white", Background: "transparent", Template: "in {{ .Name }} ", }, { Type: TIME, Style: Plain, Foreground: "p:white", Background: "transparent", Template: "at {{ .CurrentDate | date \"15:04:05\" }}", }, }, }, }, ConsoleTitleTemplate: "{{ .Shell }} in {{ .Folder }}", Palette: color.Palette{ "black": "#262B44", "blue": "#4B95E9", "green": "#59C9A5", "orange": "#F07623", "red": "#D81E5B", "white": "#E0DEF4", "yellow": "#F3AE35", }, SecondaryPrompt: &Segment{ Foreground: "p:black", Background: "transparent", Template: "\ue0b6<,p:yellow> > \ue0b0 ", }, TransientPrompt: &Segment{ Foreground: "p:black", Background: "transparent", Template: "\ue0b6<,p:yellow> {{ .Folder }} \ue0b0 ", }, Tooltips: []*Segment{ { Type: AWS, Style: Diamond, LeadingDiamond: "\ue0b0", TrailingDiamond: "\ue0b4", Foreground: "p:white", Background: "p:orange", Template: " \ue7ad {{ .Profile }}{{ if .Region }}@{{ .Region }}{{ end }} ", Options: options.Map{ options.DisplayDefault: true, }, Tips: []string{"aws"}, }, { Type: AZ, Style: Diamond, LeadingDiamond: "\ue0b0", TrailingDiamond: "\ue0b4", Foreground: "p:white", Background: "p:blue", Template: " \uebd8 {{ .Name }} ", Options: options.Map{ options.DisplayDefault: true, }, Tips: []string{"az"}, }, }, Upgrade: &upgrade.Config{ Source: upgrade.CDN, Interval: cache.ONEWEEK, }, } return cfg } func Claude() *Config { cfg := &Config{ hash: 1234567890, // placeholder hash value Version: 4, Blocks: []*Block{ { Type: Prompt, Alignment: Left, Segments: []*Segment{ { Type: PATH, Style: Diamond, LeadingDiamond: "\ue0b6", Foreground: "p:white", Background: "p:orange", Options: options.Map{ segments.DirLength: 3, segments.FolderSeparatorIcon: "\ue0bb", options.Style: "fish", }, Template: "{{ if .Segments.Git.Dir }} \uf1d2 {{ .Segments.Git.RepoName }}{{ if .Segments.Git.IsWorkTree }} \ue21c{{ end }}{{ $rel := .Segments.Git.RelativeDir }}{{ if $rel }} \ueaf7 {{ .Format $rel }}{{ end }}{{ else }} \uea83 {{ path .Path .Location }}{{ end }} ", //nolint:lll }, { Type: GIT, Style: Diamond, LeadingDiamond: "\ue0b0", TrailingDiamond: "\ue0b4", Foreground: "p:black", Background: "p:green", BackgroundTemplates: []string{ "{{ if or (.Working.Changed) (.Staging.Changed) }}p:yellow{{ end }}", "{{ if and (gt .Ahead 0) (gt .Behind 0) }}p:red{{ end }}", "{{ if gt .Ahead 0 }}#49416D{{ end }}", "{{ if gt .Behind 0 }}#7A306C{{ end }}", }, ForegroundTemplates: []string{ "{{ if or (.Working.Changed) (.Staging.Changed) }}p:black{{ end }}", "{{ if or (gt .Ahead 0) (gt .Behind 0) }}p:white{{ end }}", }, Options: options.Map{ segments.FetchStatus: true, segments.FetchUpstreamIcon: false, }, Template: " {{ if .UpstreamURL }}{{ url .UpstreamIcon .UpstreamURL }} {{ end }}{{ .HEAD }}{{if .BranchStatus }} {{ .BranchStatus }}{{ end }}{{ if .Working.Changed }} \uf044 {{ nospace .Working.String }}{{ end }}{{ if .Staging.Changed }} \uf046 {{ .Staging.String }}{{ end }} ", //nolint:lll }, }, }, { Type: Prompt, Alignment: Right, Segments: []*Segment{ { Type: CLAUDE, Style: Diamond, LeadingDiamond: "\ue0b6", TrailingDiamond: "\ue0b4", Foreground: "p:black", Background: "p:blue", Template: " \U000f0bc9 {{ .Model.DisplayName }} \uf2d0 {{ .TokenUsagePercent.Gauge }} ", }, }, }, }, Palette: color.Palette{ "black": "#262B44", "blue": "#4B95E9", "green": "#59C9A5", "orange": "#F07623", "red": "#D81E5B", "white": "#E0DEF4", "yellow": "#F3AE35", }, } return cfg } ================================================ FILE: src/config/dsc.go ================================================ package config import ( "encoding/gob" "fmt" "os" "path/filepath" "slices" "strings" "github.com/jandedobbeleer/oh-my-posh/src/dsc" "github.com/jandedobbeleer/oh-my-posh/src/log" "github.com/jandedobbeleer/oh-my-posh/src/runtime/path" ) func init() { gob.Register([]*Configuration{}) } type Resource struct { dsc.Resource[*Configuration] } func DSC() *Resource { return &Resource{ Resource: dsc.Resource[*Configuration]{}, } } type Configuration struct { Format string `json:"format,omitempty" jsonschema:"title=Format,description=The format of the configuration file,enum=json,enum=jsonc,enum=yaml,enum=yml,enum=toml,enum=tml"` Source string `json:"source,omitempty" jsonschema:"title=Source,description=The source of the configuration file"` Config resolved bool `json:"-"` } func (s *Resource) Add(configPath string) { if configPath == "" || strings.HasPrefix(configPath, "http") { log.Debug("local configuration not provided or remote configuration, skipping") return } // replace $HOME with tilde as we can't guarantee the home path configPath = filepath.Clean(configPath) configPath = strings.ReplaceAll(configPath, path.Home(), "~") s.Resource.Add(&Configuration{ Source: configPath, }) } func (s *Resource) ToJSON() string { output := s.Resource.ToJSON() return EscapeGlyphs(output, false) } func (c *Configuration) Apply() error { if c == nil { return nil } formats := map[string][]string{ JSON: {".json", ".jsonc"}, YAML: {".yaml", ".yml"}, TOML: {".toml", ".tml"}, } if !slices.Contains(formats[c.Format], filepath.Ext(c.Source)) { return fmt.Errorf("source file %s does not match format %s", c.Source, c.Format) } log.Debug("Applying configuration %s", c.Source) // Expand tilde to home directory for file operations filePath := strings.ReplaceAll(c.Source, "~", path.Home()) // Create directory if it doesn't exist dir := filepath.Dir(filePath) if err := os.MkdirAll(dir, 0755); err != nil { return fmt.Errorf("failed to create directory %s: %w", dir, err) } data := c.Export(c.Format) // Write file if err := os.WriteFile(filePath, []byte(data), 0644); err != nil { return fmt.Errorf("failed to write configuration file %s: %w", filePath, err) } log.Debug("Configuration written to %s", filePath) return nil } func (c *Configuration) Equal(config *Configuration) bool { if config == nil { return false } return c.Source == config.Source } func (c *Configuration) Resolve() (*Configuration, bool) { log.Debug("Resolving configuration %s", c.Source) if c.resolved { log.Debug("Configuration already resolved") return c, true } c.resolved = true // we use pwsh as that will never omit any feature data := Load(c.Source) if data == nil { log.Debug("No configuration data found") return nil, false } c.Config = *data c.Format = data.Format // Skip if no extends, http URL if data.Extends == "" || strings.HasPrefix(data.Extends, "http") { log.Debug("No extends found or remote configuration") return c, false } // Resolve the extends configuration parent := &Configuration{ Source: data.Extends, } return parent, true } ================================================ FILE: src/config/gob.go ================================================ package config import ( "bytes" "encoding/base64" "encoding/gob" "time" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/log" ) const ( configKey = "CONFIG" SourceKey = "CONFIG_SOURCE" ) func (cfg *Config) Store() { defer log.Trace(time.Now()) cache.Set(cache.Session, SourceKey, cfg.Source, cache.INFINITE) cache.Set(cache.Session, configKey, cfg.Base64(), cache.INFINITE) } func Get(configFile string, reload bool) *Config { defer log.Trace(time.Now()) if reload { log.Debug("reload mode enabled") if source, OK := cache.Get[string](cache.Session, SourceKey); OK { cfg := Load(source) cfg.Store() return cfg } } base64String, found := cache.Get[string](cache.Session, configKey) if !found { log.Debug("no cached config found") return Load(configFile) } var cfg Config if err := cfg.Restore(base64String); err != nil { log.Debug("failed to restore config from cache") return Load(configFile) } return &cfg } func (cfg *Config) Base64() string { defer log.Trace(time.Now()) var buffer bytes.Buffer encoder := gob.NewEncoder(&buffer) err := encoder.Encode(cfg) if err != nil { log.Error(err) return "" } return base64.StdEncoding.EncodeToString(buffer.Bytes()) } func (cfg *Config) Restore(base64String string) error { defer log.Trace(time.Now()) data, err := base64.StdEncoding.DecodeString(base64String) if err != nil { log.Error(err) return err } var buffer bytes.Buffer buffer.Write(data) decoder := gob.NewDecoder(&buffer) err = decoder.Decode(cfg) if err != nil { log.Error(err) return err } return nil } ================================================ FILE: src/config/load.go ================================================ package config import ( "bytes" "encoding/json" "errors" "fmt" "hash/fnv" "os" "path/filepath" runtimelib "runtime" "strings" "time" "github.com/gookit/goutil/jsonutil" "github.com/jandedobbeleer/oh-my-posh/src/build" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/cli/upgrade" "github.com/jandedobbeleer/oh-my-posh/src/log" "github.com/jandedobbeleer/oh-my-posh/src/runtime/http" "github.com/jandedobbeleer/oh-my-posh/src/runtime/path" toml "github.com/pelletier/go-toml/v2" yaml "go.yaml.in/yaml/v3" ) // Custom error types for config validation type Error struct { message string } func (e Error) Error() string { return fmt.Sprintf(" %s ", e.message) } var ( ErrFileNotFound = Error{"CONFIG NOT FOUND"} ErrInvalidExtension = Error{"INVALID CONFIG EXTENSION"} ErrInvalidTheme = Error{"INVALID CONFIG THEME"} ErrURLFetch = Error{"CONFIG URL FETCH FAILED"} ErrParse = Error{"CONFIG PARSE ERROR"} ErrNoConfig = Error{"NO CONFIG"} ) func Load(configFile string) *Config { defer log.Trace(time.Now()) cfg, err := Parse(configFile) if err != nil { cfg = Default(err) } return cfg } func resolveConfigLocation(config string) string { defer log.Trace(time.Now()) if strings.HasPrefix(config, "https://") { return config } if url, OK := isTheme(config); OK { log.Debug("theme detected, using theme file") return url } // Clean the config path so it works regardless of the OS config = filepath.ToSlash(config) // Cygwin path always needs the full path as we're on Windows but not really. // Doing filepath actions will convert it to a Windows path and break the init script. if isCygwin() { log.Debug("cygwin detected, using full path for config") return config } configFile := path.ReplaceTildePrefixWithHomeDir(config) abs, err := filepath.Abs(configFile) if err != nil { log.Error(err) return filepath.Clean(configFile) } return abs } type hashWriter interface { Write(p []byte) (n int, err error) } func Parse(configFile string) (*Config, error) { defer log.Trace(time.Now()) if configFile == "" { log.Debug("no config file specified") return nil, ErrNoConfig } configFile = resolveConfigLocation(configFile) configDSC := DSC() configDSC.Load() configDSC.Add(configFile) defer configDSC.Save() h := fnv.New64a() cfg, err := read(configFile, h) if err != nil { log.Errorf("failed to read config: %s", configFile) return nil, err } parentFolder := filepath.Dir(configFile) for cfg.Extends != "" { cfg.Extends = resolvePath(cfg.Extends, parentFolder) base, err := read(cfg.Extends, h) if err != nil { log.Errorf("failed to read extended config: %s", cfg.Extends) break } configDSC.Add(cfg.Extends) err = base.merge(cfg) if err != nil { log.Error(err) break } cfg = base } cfg.Source = configFile cfg.hash = h.Sum64() // Migrate segment properties to options for TOML configs // (go-toml/v2 doesn't support custom unmarshalers) cfg.migrateSegmentProperties() cfg.toggleSegments() if cfg.Upgrade == nil { cfg.Upgrade = &upgrade.Config{ Source: upgrade.CDN, DisplayNotice: cfg.UpgradeNotice, Auto: cfg.AutoUpgrade, Interval: cache.ONEWEEK, } } if cfg.Upgrade.Interval.IsEmpty() { cfg.Upgrade.Interval = cache.ONEWEEK } return cfg, nil } func resolvePath(configFile, parentFolder string) string { if url, OK := isTheme(configFile); OK { return url } if strings.HasPrefix(configFile, "https://") { return configFile } configFile = path.ReplaceTildePrefixWithHomeDir(configFile) if filepath.IsAbs(configFile) { return configFile } return filepath.Join(parentFolder, configFile) } func read(configFile string, h hashWriter) (*Config, error) { defer log.Trace(time.Now()) if configFile == "" { log.Debug("no config file specified, using default") return Default(nil), nil } var cfg Config cfg.Source = configFile cfg.Format = strings.TrimPrefix(filepath.Ext(configFile), ".") data, err := getData(configFile) if err != nil { // Determine the type of error if strings.HasPrefix(configFile, "https://") { log.Errorf("failed to fetch config from URL: %v", err) return nil, ErrURLFetch } if errors.Is(err, os.ErrNotExist) { log.Errorf("config file not found: %v", err) return nil, ErrFileNotFound } log.Errorf("failed to read config: %v", err) return nil, ErrFileNotFound } var parseErr error switch cfg.Format { case YAML, YML: cfg.Format = YAML parseErr = yaml.Unmarshal(data, &cfg) case JSONC, JSON: cfg.Format = JSON str := jsonutil.StripComments(string(data)) data = []byte(str) decoder := json.NewDecoder(bytes.NewReader(data)) parseErr = decoder.Decode(&cfg) case TOML, TML: cfg.Format = TOML parseErr = toml.Unmarshal(data, &cfg) default: log.Errorf("unsupported config file format: %s", cfg.Format) return nil, ErrInvalidExtension } if parseErr != nil { log.Errorf("failed to parse config: %v", parseErr) return nil, ErrParse } _, err = h.Write(data) if err != nil { log.Error(err) } return &cfg, nil } func getData(configFile string) ([]byte, error) { if !strings.HasPrefix(configFile, "https://") { return os.ReadFile(configFile) } return http.Download(configFile, true) } // isCygwin checks if we're running in Cygwin environment func isCygwin() bool { return runtimelib.GOOS == "windows" && len(os.Getenv("OSTYPE")) > 0 } func isTheme(config string) (string, bool) { themes := map[string]string{ "1_shell": "1_shell.omp.json", "m365princess": "M365Princess.omp.json", "agnoster": "agnoster.omp.json", "agnoster.minimal": "agnoster.minimal.omp.json", "agnosterplus": "agnosterplus.omp.json", "aliens": "aliens.omp.json", "amro": "amro.omp.json", "atomic": "atomic.omp.json", "atomicbit": "atomicBit.omp.json", "avit": "avit.omp.json", "blue-owl": "blue-owl.omp.json", "blueish": "blueish.omp.json", "bubbles": "bubbles.omp.json", "bubblesextra": "bubblesextra.omp.json", "bubblesline": "bubblesline.omp.json", "capr4n": "capr4n.omp.json", "catppuccin": "catppuccin.omp.json", "catppuccin_frappe": "catppuccin_frappe.omp.json", "catppuccin_latte": "catppuccin_latte.omp.json", "catppuccin_macchiato": "catppuccin_macchiato.omp.json", "catppuccin_mocha": "catppuccin_mocha.omp.json", "cert": "cert.omp.json", "chips": "chips.omp.json", "cinnamon": "cinnamon.omp.json", "clean-detailed": "clean-detailed.omp.json", "cloud-context": "cloud-context.omp.json", "cloud-native-azure": "cloud-native-azure.omp.json", "cobalt2": "cobalt2.omp.json", "craver": "craver.omp.json", "darkblood": "darkblood.omp.json", "devious-diamonds": "devious-diamonds.omp.yaml", "di4am0nd": "di4am0nd.omp.json", "dracula": "dracula.omp.json", "easy-term": "easy-term.omp.json", "emodipt": "emodipt.omp.json", "emodipt-extend": "emodipt-extend.omp.json", "fish": "fish.omp.json", "free-ukraine": "free-ukraine.omp.json", "froczh": "froczh.omp.json", "glowsticks": "glowsticks.omp.yaml", "gmay": "gmay.omp.json", "grandpa-style": "grandpa-style.omp.json", "gruvbox": "gruvbox.omp.json", "half-life": "half-life.omp.json", "honukai": "honukai.omp.json", "hotstick.minimal": "hotstick.minimal.omp.json", "hul10": "hul10.omp.json", "hunk": "hunk.omp.json", "huvix": "huvix.omp.json", "if_tea": "if_tea.omp.json", "illusi0n": "illusi0n.omp.json", "iterm2": "iterm2.omp.json", "jandedobbeleer": "jandedobbeleer.omp.json", "jblab_2021": "jblab_2021.omp.json", "jonnychipz": "jonnychipz.omp.json", "json": "json.omp.json", "jtracey93": "jtracey93.omp.json", "jv_sitecorian": "jv_sitecorian.omp.json", "kali": "kali.omp.json", "kushal": "kushal.omp.json", "lambda": "lambda.omp.json", "lambdageneration": "lambdageneration.omp.json", "larserikfinholt": "larserikfinholt.omp.json", "lightgreen": "lightgreen.omp.json", "marcduiker": "marcduiker.omp.json", "markbull": "markbull.omp.json", "material": "material.omp.json", "microverse-power": "microverse-power.omp.json", "mojada": "mojada.omp.json", "montys": "montys.omp.json", "mt": "mt.omp.json", "multiverse-neon": "multiverse-neon.omp.json", "negligible": "negligible.omp.json", "neko": "neko.omp.json", "night-owl": "night-owl.omp.json", "nordtron": "nordtron.omp.json", "nu4a": "nu4a.omp.json", "onehalf.minimal": "onehalf.minimal.omp.json", "paradox": "paradox.omp.json", "pararussel": "pararussel.omp.json", "patriksvensson": "patriksvensson.omp.json", "peru": "peru.omp.json", "pixelrobots": "pixelrobots.omp.json", "plague": "plague.omp.json", "poshmon": "poshmon.omp.json", "powerlevel10k_classic": "powerlevel10k_classic.omp.json", "powerlevel10k_lean": "powerlevel10k_lean.omp.json", "powerlevel10k_modern": "powerlevel10k_modern.omp.json", "powerlevel10k_rainbow": "powerlevel10k_rainbow.omp.json", "powerline": "powerline.omp.json", "probua.minimal": "probua.minimal.omp.json", "pure": "pure.omp.json", "quick-term": "quick-term.omp.json", "remk": "remk.omp.json", "robbyrussell": "robbyrussell.omp.json", "rudolfs-dark": "rudolfs-dark.omp.json", "rudolfs-light": "rudolfs-light.omp.json", "sim-web": "sim-web.omp.json", "slim": "slim.omp.json", "slimfat": "slimfat.omp.json", "smoothie": "smoothie.omp.json", "sonicboom_dark": "sonicboom_dark.omp.json", "sonicboom_light": "sonicboom_light.omp.json", "sorin": "sorin.omp.json", "space": "space.omp.json", "spaceship": "spaceship.omp.json", "star": "star.omp.json", "stelbent-compact.minimal": "stelbent-compact.minimal.omp.json", "stelbent.minimal": "stelbent.minimal.omp.json", "takuya": "takuya.omp.json", "the-unnamed": "the-unnamed.omp.json", "thecyberden": "thecyberden.omp.json", "tiwahu": "tiwahu.omp.json", "tokyo": "tokyo.omp.json", "tokyonight_storm": "tokyonight_storm.omp.json", "tonybaloney": "tonybaloney.omp.json", "uew": "uew.omp.json", "unicorn": "unicorn.omp.json", "velvet": "velvet.omp.json", "wholespace": "wholespace.omp.json", "wopian": "wopian.omp.json", "xtoys": "xtoys.omp.json", "ys": "ys.omp.json", "zash": "zash.omp.json", } themeFile, OK := themes[config] if !OK { log.Debug(config, "is not a theme") return "", false } log.Debug(config, "is a theme") if themeFilePath, err := getMSIXThemePath(themeFile); err == nil { return themeFilePath, true } log.Debug("building theme URL for:", themeFile) url := fmt.Sprintf("https://raw.githubusercontent.com/JanDeDobbeleer/oh-my-posh/refs/tags/v%s/themes/%s", build.Version, themeFile) return url, true } func getMSIXThemePath(themeFile string) (string, error) { log.Trace(time.Now(), themeFile) // For MSIX packages, the executable location is the package root exePath, err := os.Executable() if err != nil { log.Error(err) return "", err } themeFilePath := filepath.Join(filepath.Dir(exePath), "themes", themeFile) if _, err := os.Stat(themeFilePath); err != nil { log.Error(err) return "", err } log.Debug("found theme in MSIX installation:", themeFilePath) return themeFilePath, nil } ================================================ FILE: src/config/merge.go ================================================ package config import ( "errors" "reflect" "slices" "github.com/jandedobbeleer/oh-my-posh/src/log" ) type matcher interface { key() any } type matchMap[T matcher] map[any]T func (mm *matchMap[T]) hasMatch(index int, m T) (T, bool) { for _, item := range *mm { if item.key() == index { return item, true } } match, OK := (*mm)[m.key()] return match, OK } func (mm *matchMap[T]) add(m T) { if *mm == nil { *mm = make(matchMap[T]) } (*mm)[m.key()] = m } func (mm *matchMap[T]) remove(m T) { delete(*mm, m.key()) } func createMatchMap[T matcher](items []T) matchMap[T] { mm := make(matchMap[T]) for _, item := range items { if any(item) != nil { mm.add(item) } } return mm } func (cfg *Config) merge(override *Config) error { if cfg == nil || override == nil { return errors.New("configs cannot be nil") } nextExtends := cfg.Extends err := merge(override, cfg, "Blocks", "Source", "Format") if err != nil { return err } overrideBlockMap := createMatchMap(override.Blocks) for i := range cfg.Blocks { overrideBlock, exists := overrideBlockMap.hasMatch(i, cfg.Blocks[i]) if !exists { continue } // remove the block from the override map so we don't match it again overrideBlockMap.remove(overrideBlock) err = merge(overrideBlock, cfg.Blocks[i], "Segments") if err != nil { return err } overrideSegmentMap := createMatchMap(overrideBlock.Segments) for k := range cfg.Blocks[i].Segments { overrideSegment, exists := overrideSegmentMap.hasMatch(k, cfg.Blocks[i].Segments[k]) if !exists { log.Debugf("No matching segment found for %s in block %s", cfg.Blocks[i].Segments[k].Type, cfg.Blocks[i].Type) continue } // remove the block from the override map so we don't match it again overrideSegmentMap.remove(overrideSegment) baseSegment := cfg.Blocks[i].Segments[k] if baseSegment.Type != overrideSegment.Type { log.Debugf("Replacing segment %s with %s in block %s", baseSegment.Type, overrideSegment.Type, cfg.Blocks[i].Type) cfg.Blocks[i].Segments[k] = overrideSegment continue } err = merge(overrideSegment, baseSegment) if err != nil { return err } } // add any remaining segments that were not matched for _, segment := range overrideSegmentMap { log.Debugf("Adding segment %s to block %s", segment.Type, cfg.Blocks[i].Type) cfg.Blocks[i].Segments = append(cfg.Blocks[i].Segments, segment) } } cfg.Extends = nextExtends cfg.extended = true return nil } func merge(override, base any, skipFields ...string) error { if base == nil || override == nil { return errors.New("config to merge cannot be nil") } overrideValue := reflect.ValueOf(override).Elem() baseValue := reflect.ValueOf(base).Elem() overrideType := overrideValue.Type() for i := 0; i < overrideValue.NumField(); i++ { field := overrideType.Field(i) if !field.IsExported() { continue } overrideField := overrideValue.Field(i) baseField := baseValue.FieldByName(field.Name) // Skip unexported fields or fields that can't be set if isZeroValue(overrideField) || !baseField.CanSet() { continue } // Skip internal fields that shouldn't be merged if slices.Contains(skipFields, field.Name) { continue } // Special handling for slices - merge instead of replace if overrideField.Kind() == reflect.Slice { mergeSlices(overrideField, baseField) continue } // Special handling for maps - merge instead of replace if overrideField.Kind() == reflect.Map { mergeMaps(overrideField, baseField) continue } if baseField.CanSet() { baseField.Set(overrideField) } } return nil } func isZeroValue(v reflect.Value) bool { switch v.Kind() { //nolint: exhaustive case reflect.Slice, reflect.Map: return v.IsNil() || v.Len() == 0 case reflect.Ptr: return v.IsNil() case reflect.String: return v.String() == "" case reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return false default: return v.IsZero() } } func mergeSlices(override, base reflect.Value) { if base.IsNil() && !override.IsNil() { base.Set(override) return } if !base.IsNil() && !override.IsNil() { newSlice := reflect.AppendSlice(base, override) base.Set(newSlice) } } func mergeMaps(override, base reflect.Value) { if base.IsNil() && !override.IsNil() { base.Set(override) return } if !base.IsNil() && !override.IsNil() { // Merge maps - cfg values override base values for _, key := range override.MapKeys() { base.SetMapIndex(key, override.MapIndex(key)) } } if base.IsNil() { // Initialize empty map if both are nil but base has the type base.Set(reflect.MakeMap(base.Type())) } } ================================================ FILE: src/config/merge_test.go ================================================ package config import ( "testing" "github.com/jandedobbeleer/oh-my-posh/src/color" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestConfigMerge(t *testing.T) { testCases := []struct { baseConfig *Config overrideConfig *Config expectedResult *Config name string expectError bool }{ { name: "merge basic options", baseConfig: &Config{ Version: 3, FinalSpace: true, Async: false, AccentColor: "red", }, overrideConfig: &Config{ Version: 3, FinalSpace: false, Async: true, }, expectedResult: &Config{ Version: 3, FinalSpace: false, Async: true, AccentColor: "red", extended: true, }, expectError: false, }, { name: "merge with nil override", baseConfig: &Config{ Version: 3, FinalSpace: true, }, overrideConfig: nil, expectedResult: &Config{ Version: 3, FinalSpace: true, }, expectError: true, }, { name: "merge console title template", baseConfig: &Config{ ConsoleTitleTemplate: "Base Title", Version: 3, }, overrideConfig: &Config{ ConsoleTitleTemplate: "Override Title", Version: 3, }, expectedResult: &Config{ ConsoleTitleTemplate: "Override Title", Version: 3, extended: true, }, expectError: false, }, { name: "merge variables map", baseConfig: &Config{ Var: map[string]any{ "base_var": "base_value", "shared_var": "base_shared", }, Version: 3, }, overrideConfig: &Config{ Var: map[string]any{ "added_var": "added_value", "shared_var": "override_shared", }, Version: 3, }, expectedResult: &Config{ Var: map[string]any{ "base_var": "base_value", "added_var": "added_value", "shared_var": "override_shared", }, Version: 3, extended: true, }, expectError: false, }, { name: "merge blocks with matching alignment", baseConfig: &Config{ Blocks: []*Block{ { Alignment: "left", Type: "prompt", Segments: []*Segment{ {Type: "path", Options: options.Map{"style": "full"}}, }, }, }, Version: 3, }, overrideConfig: &Config{ Blocks: []*Block{ { Alignment: "left", Type: "prompt", Segments: []*Segment{ {Type: "path", Options: options.Map{"style": "short"}}, }, }, }, Version: 3, }, expectedResult: &Config{ Blocks: []*Block{ { Alignment: "left", Type: "prompt", Segments: []*Segment{ {Type: "path", Options: options.Map{"style": "short"}}, }, }, }, Version: 3, extended: true, }, expectError: false, }, { name: "merge blocks with different segment types", baseConfig: &Config{ Blocks: []*Block{ { Alignment: "left", Type: "prompt", Segments: []*Segment{ {Type: "path", Alias: "override", Options: options.Map{"style": "full"}}, }, }, }, Version: 3, }, overrideConfig: &Config{ Blocks: []*Block{ { Alignment: "left", Type: "prompt", Segments: []*Segment{ {Type: "git", Alias: "override", Options: options.Map{"branch_icon": "branch"}}, }, }, }, Version: 3, }, expectedResult: &Config{ Blocks: []*Block{ { Alignment: "left", Type: "prompt", Segments: []*Segment{ {Type: "git", Alias: "override", Options: options.Map{"branch_icon": "branch"}}, }, }, }, Version: 3, extended: true, }, expectError: false, }, { name: "merge segments by index", baseConfig: &Config{ Blocks: []*Block{ { Alignment: "left", Type: "prompt", Segments: []*Segment{ {Type: "path", Options: options.Map{"style": "full"}}, {Type: "git", Options: options.Map{"branch_icon": ""}}, }, }, }, Version: 3, }, overrideConfig: &Config{ Blocks: []*Block{ { Alignment: "left", Type: "prompt", Segments: []*Segment{ {Type: "path", Index: 1, Options: options.Map{"style": "short"}}, }, }, }, Version: 3, }, expectedResult: &Config{ Blocks: []*Block{ { Alignment: "left", Type: "prompt", Segments: []*Segment{ {Type: "path", Index: 1, Options: options.Map{"style": "short"}}, {Type: "git", Options: options.Map{"branch_icon": ""}}, }, }, }, Version: 3, extended: true, }, expectError: false, }, { name: "merge block by index", baseConfig: &Config{ Blocks: []*Block{ { Alignment: "left", Type: "prompt", Segments: []*Segment{ {Type: "path", Options: options.Map{"style": "full"}}, {Type: "git", Options: options.Map{"branch_icon": ""}}, }, }, }, Version: 3, }, overrideConfig: &Config{ Blocks: []*Block{ { Index: 1, Segments: []*Segment{ {Type: "path", Index: 1, Options: options.Map{"style": "short"}}, }, }, }, Version: 3, }, expectedResult: &Config{ Blocks: []*Block{ { Alignment: "left", Type: "prompt", Index: 1, Segments: []*Segment{ {Type: "path", Index: 1, Options: options.Map{"style": "short"}}, {Type: "git", Options: options.Map{"branch_icon": ""}}, }, }, }, Version: 3, extended: true, }, expectError: false, }, { name: "merge palette colors", baseConfig: &Config{ Palette: color.Palette{ "primary": "blue", "secondary": "green", }, Version: 3, }, overrideConfig: &Config{ Palette: color.Palette{ "primary": "red", "accent": "yellow", }, Version: 3, }, expectedResult: &Config{ Palette: color.Palette{ "primary": "red", "secondary": "green", "accent": "yellow", }, Version: 3, extended: true, }, expectError: false, }, { name: "preserve extends field", baseConfig: &Config{ Extends: "/path/to/base.json", Version: 3, }, overrideConfig: &Config{ Extends: "/path/to/override.json", Version: 3, }, expectedResult: &Config{ Extends: "/path/to/base.json", Version: 3, extended: true, }, expectError: false, }, { name: "merge tooltips slice", baseConfig: &Config{ Tooltips: []*Segment{ {Type: "git", Tips: []string{"git"}}, }, Version: 3, }, overrideConfig: &Config{ Tooltips: []*Segment{ {Type: "path", Tips: []string{"pwd"}}, }, Version: 3, }, expectedResult: &Config{ Tooltips: []*Segment{ {Type: "git", Tips: []string{"git"}}, {Type: "path", Tips: []string{"pwd"}}, }, Version: 3, extended: true, }, expectError: false, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { err := tc.baseConfig.merge(tc.overrideConfig) if tc.expectError { require.Error(t, err, tc.name) return } require.NoError(t, err, tc.name) assert.EqualExportedValues(t, tc.expectedResult, tc.baseConfig, tc.name) }) } } func TestConfigMergeEdgeCases(t *testing.T) { testCases := []struct { baseConfig *Config overrideConfig *Config name string expectError bool }{ { name: "nil base config", baseConfig: nil, overrideConfig: &Config{Version: 3}, expectError: true, }, { name: "empty configs", baseConfig: &Config{}, overrideConfig: &Config{}, expectError: false, }, { name: "override with empty blocks", baseConfig: &Config{ Blocks: []*Block{ {Alignment: "left", Type: "prompt"}, }, Version: 3, }, overrideConfig: &Config{ Blocks: []*Block{}, Version: 3, }, expectError: false, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { err := tc.baseConfig.merge(tc.overrideConfig) if tc.expectError { require.Error(t, err, tc.name) return } require.NoError(t, err, tc.name) if tc.baseConfig != nil { assert.True(t, tc.baseConfig.extended, tc.name) } }) } } ================================================ FILE: src/config/migrate_glyphs.go ================================================ package config import ( "fmt" "strconv" "strings" "github.com/jandedobbeleer/oh-my-posh/src/runtime/http" "github.com/jandedobbeleer/oh-my-posh/src/text" ) type ConnectionError struct { reason string } func (f *ConnectionError) Error() string { return f.reason } type codePoints map[uint64]uint64 func getGlyphCodePoints() (codePoints, error) { var codePoints = make(codePoints) bytes, err := http.Download("https://ohmyposh.dev/codepoints.csv", false) if err != nil { return codePoints, &ConnectionError{reason: err.Error()} } lines := strings.SplitSeq(string(bytes), "\n") for line := range lines { fields := strings.Split(line, ",") if len(fields) < 2 { continue } oldGlyph, err := strconv.ParseUint(fields[0], 16, 32) if err != nil { continue } newGlyph, err := strconv.ParseUint(fields[1], 16, 32) if err != nil { continue } codePoints[oldGlyph] = newGlyph } return codePoints, nil } func EscapeGlyphs(s string, migrate bool) string { shouldExclude := func(r rune) bool { if r < 0x1000 { // Basic Multilingual Plane return true } if r > 0x1F600 && r < 0x1F64F { // Emoticons return true } if r > 0x1F300 && r < 0x1F5FF { // Misc Symbols and Pictographs return true } if r > 0x1F680 && r < 0x1F6FF { // Transport and Map return true } if r > 0x2600 && r < 0x26FF { // Misc symbols return true } if r > 0x2700 && r < 0x27BF { // Dingbats return true } if r > 0xFE00 && r < 0xFE0F { // Variation Selectors return true } if r > 0x1F900 && r < 0x1F9FF { // Supplemental Symbols and Pictographs return true } if r > 0x1F1E6 && r < 0x1F1FF { // Flags return true } return false } var cp codePoints var err error if migrate { cp, err = getGlyphCodePoints() if err != nil { migrate = false } } sb := text.NewBuilder() for _, r := range s { // exclude regular characters and emojis if shouldExclude(r) { sb.WriteRune(r) continue } if migrate { if val, OK := cp[uint64(r)]; OK { r = rune(val) } } if r > 0x10000 { // calculate surrogate pairs one := 0xd800 + (((r - 0x10000) >> 10) & 0x3ff) two := 0xdc00 + ((r - 0x10000) & 0x3ff) quoted := fmt.Sprintf("\\u%04x\\u%04x", one, two) sb.WriteString(quoted) continue } quoted := fmt.Sprintf("\\u%04x", r) sb.WriteString(quoted) } return sb.String() } ================================================ FILE: src/config/migrate_glyphs_test.go ================================================ package config import ( "testing" "github.com/stretchr/testify/assert" ) func TestGetCodePoints(t *testing.T) { codepoints, err := getGlyphCodePoints() if connectionError, ok := err.(*ConnectionError); ok { t.Log(connectionError.Error()) return } assert.Equal(t, 1939, len(codepoints)) } func TestEscapeGlyphs(t *testing.T) { cases := []struct { Input string Expected string }{ {Input: "󰉋", Expected: "\\udb80\\ude4b"}, {Input: "a", Expected: "a"}, {Input: "\ue0b4", Expected: "\\ue0b4"}, {Input: "\ufd03", Expected: "\\ufd03"}, {Input: "}", Expected: "}"}, {Input: "🏚", Expected: "🏚"}, {Input: "\U000f0bc9", Expected: "\\udb82\\udfc9"}, {Input: "󰯉", Expected: "\\udb82\\udfc9"}, } for _, tc := range cases { assert.Equal(t, tc.Expected, EscapeGlyphs(tc.Input, false), tc.Input) } } ================================================ FILE: src/config/responsive.go ================================================ package config import "github.com/jandedobbeleer/oh-my-posh/src/runtime" func shouldHideForWidth(env runtime.Environment, minWidth, maxWidth int) bool { if maxWidth == 0 && minWidth == 0 { return false } width, err := env.TerminalWidth() if err != nil { return false } if minWidth > 0 && maxWidth > 0 { return width < minWidth || width > maxWidth } if maxWidth > 0 && width > maxWidth { return true } if minWidth > 0 && width < minWidth { return true } return false } ================================================ FILE: src/config/responsive_test.go ================================================ package config import ( "testing" "github.com/jandedobbeleer/oh-my-posh/src/runtime/mock" "github.com/stretchr/testify/assert" ) func TestShouldHideForWidth(t *testing.T) { cases := []struct { Error error Case string MinWidth int MaxWidth int Width int Expected bool }{ {Case: "No settings"}, {Case: "Min cols - hide", MinWidth: 10, Width: 9, Expected: true}, {Case: "Min cols - show", MinWidth: 10, Width: 20, Expected: false}, {Case: "Max cols - hide", MaxWidth: 10, Width: 11, Expected: true}, {Case: "Max cols - show", MaxWidth: 10, Width: 8, Expected: false}, {Case: "Min & Max cols - hide", MinWidth: 10, MaxWidth: 20, Width: 21, Expected: true}, {Case: "Min & Max cols - hide 2", MinWidth: 10, MaxWidth: 20, Width: 8, Expected: true}, {Case: "Min & Max cols - show", MinWidth: 10, MaxWidth: 20, Width: 11, Expected: false}, } for _, tc := range cases { env := new(mock.Environment) env.On("TerminalWidth").Return(tc.Width, tc.Error) got := shouldHideForWidth(env, tc.MinWidth, tc.MaxWidth) assert.Equal(t, tc.Expected, got, tc.Case) } } ================================================ FILE: src/config/segment.go ================================================ package config import ( "encoding/json" "fmt" "slices" "strings" "time" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/color" "github.com/jandedobbeleer/oh-my-posh/src/log" "github.com/jandedobbeleer/oh-my-posh/src/regex" "github.com/jandedobbeleer/oh-my-posh/src/runtime" runjobs "github.com/jandedobbeleer/oh-my-posh/src/runtime/jobs" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" "github.com/jandedobbeleer/oh-my-posh/src/template" "go.yaml.in/yaml/v3" c "golang.org/x/text/cases" "golang.org/x/text/language" ) // SegmentStyle the style of segment, for more information, see the constants type SegmentStyle string func (s *SegmentStyle) resolve(context any) SegmentStyle { value, err := template.Render(string(*s), context) // default to Plain if err != nil || value == "" { return Plain } return SegmentStyle(value) } type Segment struct { writer SegmentWriter env runtime.Environment Options options.Map `json:"options,omitempty" toml:"options,omitempty" yaml:"options,omitempty"` Properties options.Map `json:"-" toml:"properties,omitempty" yaml:"-"` Cache *Cache `json:"cache,omitempty" toml:"cache,omitempty" yaml:"cache,omitempty"` Alias string `json:"alias,omitempty" toml:"alias,omitempty" yaml:"alias,omitempty"` styleCache SegmentStyle name string LeadingDiamond string `json:"leading_diamond,omitempty" toml:"leading_diamond,omitempty" yaml:"leading_diamond,omitempty"` TrailingDiamond string `json:"trailing_diamond,omitempty" toml:"trailing_diamond,omitempty" yaml:"trailing_diamond,omitempty"` Template string `json:"template,omitempty" toml:"template,omitempty" yaml:"template,omitempty"` Foreground color.Ansi `json:"foreground,omitempty" toml:"foreground,omitempty" yaml:"foreground,omitempty"` TemplatesLogic template.Logic `json:"templates_logic,omitempty" toml:"templates_logic,omitempty" yaml:"templates_logic,omitempty"` PowerlineSymbol string `json:"powerline_symbol,omitempty" toml:"powerline_symbol,omitempty" yaml:"powerline_symbol,omitempty"` Background color.Ansi `json:"background,omitempty" toml:"background,omitempty" yaml:"background,omitempty"` Filler string `json:"filler,omitempty" toml:"filler,omitempty" yaml:"filler,omitempty"` Type SegmentType `json:"type,omitempty" toml:"type,omitempty" yaml:"type,omitempty"` Style SegmentStyle `json:"style,omitempty" toml:"style,omitempty" yaml:"style,omitempty"` LeadingPowerlineSymbol string `json:"leading_powerline_symbol,omitempty" toml:"leading_powerline_symbol,omitempty" yaml:"leading_powerline_symbol,omitempty"` Placeholder string `json:"placeholder,omitempty" toml:"placeholder,omitempty" yaml:"placeholder,omitempty"` Tips []string `json:"tips,omitempty" toml:"tips,omitempty" yaml:"tips,omitempty"` BackgroundTemplates template.List `json:"background_templates,omitempty" toml:"background_templates,omitempty" yaml:"background_templates,omitempty"` Templates template.List `json:"templates,omitempty" toml:"templates,omitempty" yaml:"templates,omitempty"` ExcludeFolders []string `json:"exclude_folders,omitempty" toml:"exclude_folders,omitempty" yaml:"exclude_folders,omitempty"` IncludeFolders []string `json:"include_folders,omitempty" toml:"include_folders,omitempty" yaml:"include_folders,omitempty"` Needs []string `json:"-" toml:"-" yaml:"-"` ForegroundTemplates template.List `json:"foreground_templates,omitempty" toml:"foreground_templates,omitempty" yaml:"foreground_templates,omitempty"` Index int `json:"index,omitempty" toml:"index,omitempty" yaml:"index,omitempty"` MinWidth int `json:"min_width,omitempty" toml:"min_width,omitempty" yaml:"min_width,omitempty"` Duration time.Duration `json:"-" toml:"-" yaml:"-"` NameLength int `json:"-" toml:"-" yaml:"-"` MaxWidth int `json:"max_width,omitempty" toml:"max_width,omitempty" yaml:"max_width,omitempty"` Timeout int `json:"timeout,omitempty" toml:"timeout,omitempty" yaml:"timeout,omitempty"` Newline bool `json:"newline,omitempty" toml:"newline,omitempty" yaml:"newline,omitempty"` Enabled bool `json:"-" toml:"-" yaml:"-"` InvertPowerline bool `json:"invert_powerline,omitempty" toml:"invert_powerline,omitempty" yaml:"invert_powerline,omitempty"` Force bool `json:"force,omitempty" toml:"force,omitempty" yaml:"force,omitempty"` restored bool `json:"-" toml:"-" yaml:"-"` Toggled bool `json:"toggled,omitempty" toml:"toggled,omitempty" yaml:"toggled,omitempty"` Pending bool `json:"-" toml:"-" yaml:"-"` Interactive bool `json:"interactive,omitempty" toml:"interactive,omitempty" yaml:"interactive,omitempty"` } // segmentAlias is used to avoid recursion during unmarshaling type segmentAlias Segment // segmentAux is a helper struct that captures the legacy 'properties' field type segmentAux struct { Properties options.Map `json:"properties,omitempty" yaml:"properties,omitempty" toml:"properties,omitempty"` *segmentAlias } func (segment *Segment) UnmarshalJSON(data []byte) error { aux := &segmentAux{ segmentAlias: (*segmentAlias)(segment), } if err := json.Unmarshal(data, aux); err != nil { return err } // Migrate 'properties' to 'options' if present if len(aux.Properties) > 0 && len(segment.Options) == 0 { segment.Options = aux.Properties } return nil } func (segment *Segment) UnmarshalYAML(node *yaml.Node) error { // Decode into a map to handle field renaming var raw map[string]any if err := node.Decode(&raw); err != nil { return err } // If 'properties' exists and 'options' doesn't, rename it if props, hasProps := raw["properties"]; hasProps { if _, hasOptions := raw["options"]; !hasOptions { raw["options"] = props delete(raw, "properties") } } // Re-encode and decode into the struct modifiedNode := &yaml.Node{} if err := modifiedNode.Encode(raw); err != nil { return err } return modifiedNode.Decode((*segmentAlias)(segment)) } // MigratePropertiesToOptions migrates the deprecated Properties field to Options. // This is needed for TOML configs since go-toml/v2 doesn't support custom unmarshalers. func (segment *Segment) MigratePropertiesToOptions() { if len(segment.Properties) > 0 && len(segment.Options) == 0 { segment.Options = segment.Properties segment.Properties = nil } } func (segment *Segment) Name() string { if len(segment.name) != 0 { return segment.name } name := segment.Alias if name == "" { name = c.Title(language.English).String(string(segment.Type)) } segment.name = name return name } func (segment *Segment) Execute(env runtime.Environment) { // segment timings for debug purposes var start time.Time if env.Flags().Debug { start = time.Now() segment.NameLength = len(segment.Name()) defer func() { segment.Duration = time.Since(start) }() } defer segment.evaluateNeeds() err := segment.MapSegmentWithWriter(env) if err != nil || !segment.shouldIncludeFolder() { return } log.Debugf("segment: %s", segment.Name()) if segment.isToggled() { return } cacheRestored := segment.restoreCache() if cacheRestored && !env.Flags().Streaming { return } if shouldHideForWidth(segment.env, segment.MinWidth, segment.MaxWidth) { return } defer func() { if segment.Enabled { template.Cache.AddSegmentData(segment.Name(), segment.writer) } }() // Create Job for this goroutine so child processes can be tracked and killed on timeout if err := runjobs.CreateJobForGoroutine(segment.Name()); err != nil { log.Errorf("failed to create job for goroutine (segment: %s): %v", segment.Name(), err) } segment.Enabled = segment.writer.Enabled() } func (segment *Segment) Render(index int, force bool) bool { // Allow pending segments to render (they'll show "..." text) if !segment.Pending && !segment.Enabled && !force { return false } if force { segment.Force = true } segment.writer.SetIndex(index) text := segment.string() // Only update Enabled if segment is NOT pending (avoid race with Execute goroutine) if !segment.Pending { segment.Enabled = segment.Force || len(strings.ReplaceAll(text, " ", "")) > 0 if !segment.Enabled { template.Cache.RemoveSegmentData(segment.Name()) return false } } segment.SetText(text) segment.setCache() // We do this to make `.Text` available for a cross-segment reference in an extra prompt. template.Cache.AddSegmentData(segment.Name(), segment.writer) return true } func (segment *Segment) Text() string { return segment.writer.Text() } func (segment *Segment) SetText(text string) { segment.writer.SetText(text) } func (segment *Segment) ResolveForeground() color.Ansi { if len(segment.ForegroundTemplates) != 0 { match := segment.ForegroundTemplates.FirstMatch(segment.writer, segment.Foreground.String()) segment.Foreground = color.Ansi(match) } return segment.Foreground } func (segment *Segment) ResolveBackground() color.Ansi { if len(segment.BackgroundTemplates) != 0 { match := segment.BackgroundTemplates.FirstMatch(segment.writer, segment.Background.String()) segment.Background = color.Ansi(match) } return segment.Background } func (segment *Segment) ResolveStyle() SegmentStyle { if len(segment.styleCache) != 0 { return segment.styleCache } segment.styleCache = segment.Style.resolve(segment.writer) return segment.styleCache } func (segment *Segment) IsPowerline() bool { style := segment.ResolveStyle() return style == Powerline || style == Accordion } func (segment *Segment) HasEmptyDiamondAtEnd() bool { if segment.ResolveStyle() != Diamond { return false } return segment.TrailingDiamond == "" } func (segment *Segment) hasCache() bool { return segment.Cache != nil && !segment.Cache.Duration.IsEmpty() } func (segment *Segment) isToggled() bool { togglesMap, OK := cache.Get[map[string]bool](cache.Session, cache.TOGGLECACHE) if !OK || len(togglesMap) == 0 { log.Debug("no toggles found") return false } segmentName := segment.Alias if segmentName == "" { segmentName = string(segment.Type) } if togglesMap[segmentName] { log.Debugf("segment toggled off: %s", segment.Name()) return true } return false } func (segment *Segment) restoreCache() bool { if !segment.hasCache() { return false } key, store := segment.cacheKeyAndStore() data, OK := cache.Get[string](store, key) if !OK { log.Debugf("no cache found for segment: %s, key: %s", segment.Name(), key) return false } err := json.Unmarshal([]byte(data), &segment.writer) if err != nil { log.Error(err) } segment.Enabled = true template.Cache.AddSegmentData(segment.Name(), segment.writer) log.Debug("restored segment from cache: ", segment.Name()) segment.restored = true return true } func (segment *Segment) setCache() { if segment.restored || !segment.hasCache() { return } // Never cache pending state to avoid polluting cache with incomplete data if segment.Pending { return } data, err := json.Marshal(segment.writer) if err != nil { log.Error(err) return } // TODO: check if we can make segmentwriter a generic Type indicator // that way we can actually get the value straight from cache.Get // and marchalling is obsolete key, store := segment.cacheKeyAndStore() cache.Set(store, key, string(data), segment.Cache.Duration) } func (segment *Segment) cacheKeyAndStore() (string, cache.Store) { format := "segment_cache_%s" switch segment.Cache.Strategy { case Session: return fmt.Sprintf(format, segment.Name()), cache.Session case Device: return fmt.Sprintf(format, segment.Name()), cache.Device case Folder: fallthrough default: return fmt.Sprintf(format, strings.Join([]string{segment.Name(), segment.folderKey()}, "_")), cache.Device } } func (segment *Segment) folderKey() string { key, ok := segment.writer.CacheKey() if !ok { return segment.env.Pwd() } return key } func (segment *Segment) string() string { // Use simple pending text if segment is still pending if segment.Pending { if segment.Placeholder != "" { return segment.Placeholder } return "..." } result := segment.Templates.Resolve(segment.writer, "", segment.TemplatesLogic) if len(result) != 0 { return result } if segment.Template == "" { segment.Template = segment.writer.Template() } text, err := template.Render(segment.Template, segment.writer) if err != nil { return err.Error() } return text } func (segment *Segment) shouldIncludeFolder() bool { if segment.env == nil { return true } cwdIncluded := segment.cwdIncluded() cwdExcluded := segment.cwdExcluded() return cwdIncluded && !cwdExcluded } func (segment *Segment) cwdIncluded() bool { if len(segment.IncludeFolders) == 0 { return true } return segment.env.DirMatchesOneOf(segment.env.Pwd(), segment.IncludeFolders) } func (segment *Segment) cwdExcluded() bool { return segment.env.DirMatchesOneOf(segment.env.Pwd(), segment.ExcludeFolders) } func (segment *Segment) evaluateNeeds() { value := segment.Template if len(segment.ForegroundTemplates) != 0 { value += strings.Join(segment.ForegroundTemplates, "") } if len(segment.BackgroundTemplates) != 0 { value += strings.Join(segment.BackgroundTemplates, "") } if len(segment.Templates) != 0 { value += strings.Join(segment.Templates, "") } if !strings.Contains(value, ".Segments.") { return } matches := regex.FindAllNamedRegexMatch(`\.Segments\.(?P[a-zA-Z0-9]+)`, value) for _, name := range matches { segmentName := name["NAME"] if len(name) == 0 || slices.Contains(segment.Needs, segmentName) { continue } segment.Needs = append(segment.Needs, segmentName) } } func (segment *Segment) key() any { if segment.Index > 0 { return segment.Index - 1 } return segment.Name() } ================================================ FILE: src/config/segment_test.go ================================================ package config import ( "encoding/json" "testing" "github.com/jandedobbeleer/oh-my-posh/src/color" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/runtime/mock" "github.com/jandedobbeleer/oh-my-posh/src/segments" toml "github.com/pelletier/go-toml/v2" "github.com/stretchr/testify/assert" "go.yaml.in/yaml/v3" ) const ( cwd = "Projects/oh-my-posh" ) func TestMapSegmentWriterCanMap(t *testing.T) { sc := &Segment{ Type: SESSION, } env := new(mock.Environment) err := sc.MapSegmentWithWriter(env) assert.NoError(t, err) assert.NotNil(t, sc.writer) } func TestMapSegmentWriterCannotMap(t *testing.T) { sc := &Segment{ Type: "nilwriter", } env := new(mock.Environment) err := sc.MapSegmentWithWriter(env) assert.Error(t, err) } func TestParseTestConfig(t *testing.T) { segmentJSON := ` { "type": "path", "style": "powerline", "powerline_symbol": "\uE0B0", "foreground": "#ffffff", "background": "#61AFEF", "options": { "style": "folder" }, "exclude_folders": [ "/super/secret/project" ] } ` segment := &Segment{} err := json.Unmarshal([]byte(segmentJSON), segment) assert.NoError(t, err) assert.NotNil(t, segment.Options) assert.Equal(t, "folder", segment.Options.String("style", "")) } func TestParseConfigWithOptions(t *testing.T) { segmentJSON := ` { "type": "path", "style": "powerline", "options": { "style": "folder" } } ` segment := &Segment{} err := json.Unmarshal([]byte(segmentJSON), segment) assert.NoError(t, err) assert.NotNil(t, segment.Options) assert.Equal(t, "folder", segment.Options.String("style", "")) } func TestParseYAMLConfigWithProperties(t *testing.T) { segmentYAML := ` type: path style: powerline properties: style: folder ` segment := &Segment{} err := yaml.Unmarshal([]byte(segmentYAML), segment) assert.NoError(t, err) assert.NotNil(t, segment.Options) assert.Equal(t, "folder", segment.Options.String("style", "")) } func TestParseYAMLConfigWithOptions(t *testing.T) { segmentYAML := ` type: path style: powerline options: style: folder ` segment := &Segment{} err := yaml.Unmarshal([]byte(segmentYAML), segment) assert.NoError(t, err) assert.NotNil(t, segment.Options) assert.Equal(t, "folder", segment.Options.String("style", "")) } func TestParseTOMLConfigWithProperties(t *testing.T) { segmentTOML := ` type = "path" style = "powerline" [properties] style = "folder" ` segment := &Segment{} err := toml.Unmarshal([]byte(segmentTOML), segment) assert.NoError(t, err) // Migrate properties to options (normally done by Config.migrateSegmentProperties) segment.MigratePropertiesToOptions() assert.NotNil(t, segment.Options) assert.Equal(t, "folder", segment.Options.String("style", "")) } func TestParseTOMLConfigWithOptions(t *testing.T) { segmentTOML := ` type = "path" style = "powerline" [options] style = "folder" ` segment := &Segment{} err := toml.Unmarshal([]byte(segmentTOML), segment) assert.NoError(t, err) // Migrate properties to options (should be a no-op since options is set) segment.MigratePropertiesToOptions() assert.NotNil(t, segment.Options) assert.Equal(t, "folder", segment.Options.String("style", "")) } func TestParseTOMLConfigWithBothOptionsAndProperties(t *testing.T) { // If both are specified, options takes precedence segmentTOML := ` type = "path" style = "powerline" [options] style = "folder" [properties] style = "letter" ` segment := &Segment{} err := toml.Unmarshal([]byte(segmentTOML), segment) assert.NoError(t, err) // Migrate should not overwrite options segment.MigratePropertiesToOptions() assert.NotNil(t, segment.Options) assert.Equal(t, "folder", segment.Options.String("style", "")) } func TestShouldIncludeFolder(t *testing.T) { cases := []struct { Case string Included bool Excluded bool Expected bool }{ {Case: "Include", Included: true, Excluded: false, Expected: true}, {Case: "Exclude", Included: false, Excluded: true, Expected: false}, {Case: "Include & Exclude", Included: true, Excluded: true, Expected: false}, {Case: "!Include & !Exclude", Included: false, Excluded: false, Expected: false}, } for _, tc := range cases { env := new(mock.Environment) env.On("GOOS").Return(runtime.LINUX) env.On("Home").Return("") env.On("Pwd").Return(cwd) env.On("DirMatchesOneOf", cwd, []string{"Projects/oh-my-posh"}).Return(tc.Included) env.On("DirMatchesOneOf", cwd, []string{"Projects/nope"}).Return(tc.Excluded) segment := &Segment{ IncludeFolders: []string{"Projects/oh-my-posh"}, ExcludeFolders: []string{"Projects/nope"}, env: env, } got := segment.shouldIncludeFolder() assert.Equal(t, tc.Expected, got, tc.Case) } } func TestGetColors(t *testing.T) { cases := []struct { Case string Expected color.Ansi Default color.Ansi Region string Profile string Templates []string Background bool }{ {Case: "No template - foreground", Expected: "color", Background: false, Default: "color"}, {Case: "No template - background", Expected: "color", Background: true, Default: "color"}, {Case: "Nil template", Expected: "color", Default: "color", Templates: nil}, { Case: "Template - default", Expected: "color", Default: "color", Templates: []string{ "{{if contains \"john\" .Profile}}color2{{end}}", }, Profile: "doe", }, { Case: "Template - override", Expected: "color2", Default: "color", Templates: []string{ "{{if contains \"john\" .Profile}}color2{{end}}", }, Profile: "john", }, { Case: "Template - override multiple", Expected: "color3", Default: "color", Templates: []string{ "{{if contains \"doe\" .Profile}}color2{{end}}", "{{if contains \"john\" .Profile}}color3{{end}}", }, Profile: "john", }, { Case: "Template - override multiple no match", Expected: "color", Default: "color", Templates: []string{ "{{if contains \"doe\" .Profile}}color2{{end}}", "{{if contains \"philip\" .Profile}}color3{{end}}", }, Profile: "john", }, } for _, tc := range cases { segment := &Segment{ writer: &segments.Aws{ Profile: tc.Profile, Region: tc.Region, }, } if tc.Background { segment.Background = tc.Default segment.BackgroundTemplates = tc.Templates bgColor := segment.ResolveBackground() assert.Equal(t, tc.Expected, bgColor, tc.Case) continue } segment.Foreground = tc.Default segment.ForegroundTemplates = tc.Templates fgColor := segment.ResolveForeground() assert.Equal(t, tc.Expected, fgColor, tc.Case) } } func TestEvaluateNeeds(t *testing.T) { cases := []struct { Segment *Segment Case string Needs []string }{ { Case: "No needs", Segment: &Segment{ Template: "foo", }, }, { Case: "Template needs", Segment: &Segment{ Template: "{{ .Segments.Git.URL }}", }, Needs: []string{"Git"}, }, { Case: "Template & Foreground needs", Segment: &Segment{ Template: "{{ .Segments.Git.URL }}", ForegroundTemplates: []string{"foo", "{{ .Segments.Os.Icon }}"}, }, Needs: []string{"Git", "Os"}, }, { Case: "Template & Foreground & Background needs", Segment: &Segment{ Template: "{{ .Segments.Git.URL }}", ForegroundTemplates: []string{"foo", "{{ .Segments.Os.Icon }}"}, BackgroundTemplates: []string{"bar", "{{ .Segments.Exit.Icon }}"}, }, Needs: []string{"Git", "Os", "Exit"}, }, } for _, tc := range cases { tc.Segment.evaluateNeeds() assert.Equal(t, tc.Needs, tc.Segment.Needs, tc.Case) } } func TestSegment_NoCachingWhenPending(t *testing.T) { env := new(mock.Environment) env.On("Shell").Return("pwsh") env.On("Flags").Return(&runtime.Flags{}) env.On("Pwd").Return("/test") env.On("Home").Return("/home") segment := &Segment{ Type: SESSION, Pending: true, Template: "test", } err := segment.MapSegmentWithWriter(env) assert.NoError(t, err) // When Pending=true, setCache should return early without caching // We can't easily mock cache.Set, but we can verify the method doesn't panic // and that the behavior differs between Pending=true and Pending=false // With Pending=true, setCache returns early segment.Cache = &Cache{Duration: "5h"} segment.setCache() // Should return early, not attempt to cache // Verify this doesn't panic and segment still works assert.True(t, segment.Pending, "Segment should still be pending") // Now with Pending=false, setCache will attempt to cache segment.Pending = false segment.restored = false segment.setCache() // Should attempt to cache (may fail but shouldn't panic) assert.False(t, segment.Pending, "Segment should not be pending") } ================================================ FILE: src/config/segment_types.go ================================================ package config import ( "encoding/gob" "errors" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/segments" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" ) // SegmentType the type of segment, for more information, see the constants type SegmentType string // SegmentWriter is the interface used to define what and if to write to the prompt type SegmentWriter interface { Enabled() bool Template() string SetText(text string) SetIndex(index int) Text() string Init(props options.Provider, env runtime.Environment) CacheKey() (string, bool) } func init() { gob.Register(&segments.Angular{}) gob.Register(&segments.Version{}) gob.Register(&segments.Argocd{}) gob.Register(&segments.Aurelia{}) gob.Register(&segments.Aws{}) gob.Register(&segments.Az{}) gob.Register(&segments.Azd{}) gob.Register(&segments.AzFunc{}) gob.Register(&segments.Battery{}) gob.Register(&segments.Bazel{}) gob.Register(&segments.Brewfather{}) gob.Register(&segments.Buf{}) gob.Register(&segments.Bun{}) gob.Register(&segments.CarbonIntensity{}) gob.Register(&segments.Cds{}) gob.Register(&segments.Copilot{}) gob.Register(&segments.Cf{}) gob.Register(&segments.CfTarget{}) gob.Register(&segments.Claude{}) gob.Register(&segments.ClaudeData{}) gob.Register(&segments.Clojure{}) gob.Register(&segments.Cmake{}) gob.Register(&segments.Connection{}) gob.Register(&segments.Crystal{}) gob.Register(&segments.Dart{}) gob.Register(&segments.Deno{}) gob.Register(&segments.Docker{}) gob.Register(&segments.Dotnet{}) gob.Register(&segments.Elixir{}) gob.Register(&segments.Executiontime{}) gob.Register(&segments.Status{}) gob.Register(&segments.Firebase{}) gob.Register(&segments.Flutter{}) gob.Register(&segments.Fortran{}) gob.Register(&segments.Fossil{}) gob.Register(&segments.FossilStatus{}) gob.Register(&segments.Gcp{}) gob.Register(&segments.Git{}) gob.Register(&segments.GitStatus{}) gob.Register(&segments.Rebase{}) gob.Register(&segments.User{}) gob.Register(&segments.Commit{}) gob.Register(&segments.GitVersion{}) gob.Register(&segments.Golang{}) gob.Register(&segments.Haskell{}) gob.Register(&segments.Helm{}) gob.Register(&segments.IPify{}) gob.Register(&segments.Java{}) gob.Register(&segments.HTTP{}) gob.Register(&segments.Jujutsu{}) gob.Register(&segments.JujutsuStatus{}) gob.Register(&segments.Julia{}) gob.Register(&segments.Kotlin{}) gob.Register(&segments.Kubectl{}) gob.Register(&segments.LastFM{}) gob.Register(&segments.Lua{}) gob.Register(&segments.Mercurial{}) gob.Register(&segments.MercurialStatus{}) gob.Register(&segments.Mojo{}) gob.Register(&segments.Mvn{}) gob.Register(&segments.Nba{}) gob.Register(&segments.Nbgv{}) gob.Register(&segments.Nightscout{}) gob.Register(&segments.NixShell{}) gob.Register(&segments.Nim{}) gob.Register(&segments.Node{}) gob.Register(&segments.Npm{}) gob.Register(&segments.Nx{}) gob.Register(&segments.OCaml{}) gob.Register(&segments.Os{}) gob.Register(&segments.Owm{}) gob.Register(&segments.Path{}) gob.Register(&segments.Folders{}) gob.Register(&segments.Perl{}) gob.Register(&segments.Php{}) gob.Register(&segments.Plastic{}) gob.Register(&segments.PlasticStatus{}) gob.Register(&segments.Pnpm{}) gob.Register(&segments.Project{}) gob.Register(&segments.Pulumi{}) gob.Register(&segments.Python{}) gob.Register(&segments.Quasar{}) gob.Register(&segments.Package{}) gob.Register(&segments.R{}) gob.Register(&segments.Ramadan{}) gob.Register(&segments.React{}) gob.Register(&segments.Root{}) gob.Register(&segments.Ruby{}) gob.Register(&segments.Rust{}) gob.Register(&segments.Sapling{}) gob.Register(&segments.SaplingStatus{}) gob.Register(&segments.Session{}) gob.Register(&segments.Shell{}) gob.Register(&segments.Sitecore{}) gob.Register(&segments.Spotify{}) gob.Register(&segments.Status{}) gob.Register(&segments.Strava{}) gob.Register(&segments.Svelte{}) gob.Register(&segments.Svn{}) gob.Register(&segments.SvnStatus{}) gob.Register(&segments.Swift{}) gob.Register(&segments.SystemInfo{}) gob.Register(&segments.TalosCTL{}) gob.Register(&segments.Taskwarrior{}) gob.Register(&segments.Tauri{}) gob.Register(&segments.Terraform{}) gob.Register(&segments.Text{}) gob.Register(&segments.Time{}) gob.Register(&segments.Todoist{}) gob.Register(&segments.UI5Tooling{}) gob.Register(&segments.Umbraco{}) gob.Register(&segments.Unity{}) gob.Register(&segments.Upgrade{}) gob.Register(&segments.UpgradeCache{}) gob.Register(&segments.V{}) gob.Register(&segments.Vala{}) gob.Register(&segments.Wakatime{}) gob.Register(&segments.WinGet{}) gob.Register(&segments.WinGetPackage{}) gob.Register(&segments.WindowsRegistry{}) gob.Register(&segments.Withings{}) gob.Register(&segments.XMake{}) gob.Register(&segments.Yarn{}) gob.Register(&segments.Ytm{}) gob.Register(&segments.Zig{}) gob.Register(&segments.Segment{}) } const ( // Plain writes it without ornaments Plain SegmentStyle = "plain" // Powerline writes it Powerline style Powerline SegmentStyle = "powerline" // Accordion writes it Powerline style but collapses the segment when disabled instead of hiding Accordion SegmentStyle = "accordion" // Diamond writes the prompt shaped with a leading and trailing symbol Diamond SegmentStyle = "diamond" // ANGULAR writes which angular cli version us currently active ANGULAR SegmentType = "angular" // ARGOCD writes the current argocd context ARGOCD SegmentType = "argocd" // AURELIA writes which aurelia version is currently referenced in package.json AURELIA SegmentType = "aurelia" // AWS writes the active aws context AWS SegmentType = "aws" // AZ writes the Azure subscription info we're currently in AZ SegmentType = "az" // AZD writes the Azure Developer CLI environment info we're current in AZD SegmentType = "azd" // AZFUNC writes current AZ func version AZFUNC SegmentType = "azfunc" // BATTERY writes the battery percentage BATTERY SegmentType = "battery" // BAZEL writes the bazel version BAZEL SegmentType = "bazel" // Brewfather segment BREWFATHER SegmentType = "brewfather" // Buf segment writes the active buf version BUF SegmentType = "buf" // BUN writes the active bun version BUN SegmentType = "bun" // CARBONINTENSITY writes the actual and forecast carbon intensity in gCO2/kWh CARBONINTENSITY SegmentType = "carbonintensity" // cds (SAP CAP) version CDS SegmentType = "cds" // Cloud Foundry segment CF SegmentType = "cf" // Cloud Foundry logged in target CFTARGET SegmentType = "cftarget" // CLAUDE writes Claude Code session information CLAUDE SegmentType = "claude" // CLOJURE writes the active clojure version CLOJURE SegmentType = "clojure" // CMAKE writes the active cmake version CMAKE SegmentType = "cmake" // CONNECTION writes a connection's information CONNECTION SegmentType = "connection" // COPILOT writes GitHub Copilot usage statistics COPILOT SegmentType = "copilot" // CRYSTAL writes the active crystal version CRYSTAL SegmentType = "crystal" // DART writes the active dart version DART SegmentType = "dart" // DENO writes the active deno version DENO SegmentType = "deno" // DOCKER writes the docker context DOCKER SegmentType = "docker" // DOTNET writes which dotnet version is currently active DOTNET SegmentType = "dotnet" // ELIXIR writes the elixir version ELIXIR SegmentType = "elixir" // EXECUTIONTIME writes the execution time of the last run command EXECUTIONTIME SegmentType = "executiontime" // EXIT writes the last exit code EXIT SegmentType = "exit" // FIREBASE writes the active firebase project FIREBASE SegmentType = "firebase" // FLUTTER writes the flutter version FLUTTER SegmentType = "flutter" // FORTRAN writes the gfortran version FORTRAN SegmentType = "fortran" // FOSSIL writes the fossil status FOSSIL SegmentType = "fossil" // GCP writes the active GCP context GCP SegmentType = "gcp" // GIT represents the git status and information GIT SegmentType = "git" // GITVERSION represents the gitversion information GITVERSION SegmentType = "gitversion" // GOLANG writes which go version is currently active GOLANG SegmentType = "go" // HASKELL segment HASKELL SegmentType = "haskell" // HELM segment HELM SegmentType = "helm" // IPIFY segment IPIFY SegmentType = "ipify" // JAVA writes the active java version JAVA SegmentType = "java" // API writes the output of a custom JSON API HTTP SegmentType = "http" // JUJUTSU writes Jujutsu source control information JUJUTSU SegmentType = "jujutsu" // JULIA writes which julia version is currently active JULIA SegmentType = "julia" // KOTLIN writes the active kotlin version KOTLIN SegmentType = "kotlin" // KUBECTL writes the Kubernetes context we're currently in KUBECTL SegmentType = "kubectl" // LASTFM writes the lastfm status LASTFM SegmentType = "lastfm" // LUA writes the active lua version LUA SegmentType = "lua" // MERCURIAL writes Mercurial source control information MERCURIAL SegmentType = "mercurial" // MOJO writes the active version of Mojo and the name of the Magic virtual env MOJO SegmentType = "mojo" // MVN writes the active maven version MVN SegmentType = "mvn" // NBA writes NBA game data NBA SegmentType = "nba" // NBGV writes the nbgv version information NBGV SegmentType = "nbgv" // NIGHTSCOUT is an open source diabetes system NIGHTSCOUT SegmentType = "nightscout" // NIM writes the active nim version NIM SegmentType = "nim" // NIXSHELL writes the active nix shell details NIXSHELL SegmentType = "nix-shell" // NODE writes which node version is currently active NODE SegmentType = "node" // npm version NPM SegmentType = "npm" // NX writes which Nx version us currently active NX SegmentType = "nx" // OCAML writes the active Ocaml version OCAML SegmentType = "ocaml" // OS write os specific icon OS SegmentType = "os" // OWM writes the weather coming from openweatherdata OWM SegmentType = "owm" // PATH represents the current path segment PATH SegmentType = "path" // PERL writes which perl version is currently active PERL SegmentType = "perl" // PHP writes which php version is currently active PHP SegmentType = "php" // PLASTIC represents the plastic scm status and information PLASTIC SegmentType = "plastic" // pnpm version PNPM SegmentType = "pnpm" // Project version PROJECT SegmentType = "project" // PULUMI writes the pulumi user, store and stack PULUMI SegmentType = "pulumi" // PYTHON writes the virtual env name PYTHON SegmentType = "python" // QUASAR writes the QUASAR version and context QUASAR SegmentType = "quasar" // R version R SegmentType = "r" // RAMADAN displays Sehar and Iftar prayer times during Ramadan RAMADAN SegmentType = "ramadan" // REACT writes the current react version REACT SegmentType = "react" // ROOT writes root symbol ROOT SegmentType = "root" // RUBY writes which ruby version is currently active RUBY SegmentType = "ruby" // RUST writes the cargo version information if cargo.toml is present RUST SegmentType = "rust" // SAPLING represents the sapling segment SAPLING SegmentType = "sapling" // SESSION represents the user info segment SESSION SegmentType = "session" // SHELL writes which shell we're currently in SHELL SegmentType = "shell" // SITECORE displays the current context for the Sitecore CLI SITECORE SegmentType = "sitecore" // SPOTIFY writes the SPOTIFY status for Mac SPOTIFY SegmentType = "spotify" // STATUS writes the last know command status STATUS SegmentType = "status" // STRAVA is a sports activity tracker STRAVA SegmentType = "strava" // Svelte segment SVELTE SegmentType = "svelte" // Subversion segment SVN SegmentType = "svn" // SWIFT writes the active swift version SWIFT SegmentType = "swift" // SYSTEMINFO writes system information (memory, cpu, load) SYSTEMINFO SegmentType = "sysinfo" // TALOSCTL writes the talosctl context TALOSCTL SegmentType = "talosctl" // TASKWARRIOR writes Taskwarrior task counts and context TASKWARRIOR SegmentType = "taskwarrior" // Tauri Segment TAURI SegmentType = "tauri" // TERRAFORM writes the terraform workspace we're currently in TERRAFORM SegmentType = "terraform" // TEXT writes a text TEXT SegmentType = "text" // TIME writes the current timestamp TIME SegmentType = "time" // TODOIST segment TODOIST SegmentType = "todoist" // UI5 Tooling segment UI5TOOLING SegmentType = "ui5tooling" // UMBRACO writes the Umbraco version if Umbraco is present UMBRACO SegmentType = "umbraco" // UNITY writes which Unity version is currently active UNITY SegmentType = "unity" // UPGRADE lets you know if you can upgrade Oh My Posh UPGRADE SegmentType = "upgrade" // V writes the active vlang version V SegmentType = "v" // VALA writes the active vala version VALA SegmentType = "vala" // WAKATIME writes tracked time spend in dev editors WAKATIME SegmentType = "wakatime" // WINGET writes the number of available WinGet package updates WINGET SegmentType = "winget" // WINREG queries the Windows registry. WINREG SegmentType = "winreg" // WITHINGS queries the Withings API. WITHINGS SegmentType = "withings" // XMAKE write the xmake version if xmake.lua is present XMAKE SegmentType = "xmake" // yarn version YARN SegmentType = "yarn" // YTM writes YouTube Music information and status YTM SegmentType = "ytm" // ZIG writes the active zig version ZIG SegmentType = "zig" ) // Segments contains all available prompt segment writers. // Consumers of the library can also add their own segment writer. var Segments = map[SegmentType]func() SegmentWriter{ ANGULAR: func() SegmentWriter { return &segments.Angular{} }, ARGOCD: func() SegmentWriter { return &segments.Argocd{} }, AURELIA: func() SegmentWriter { return &segments.Aurelia{} }, AWS: func() SegmentWriter { return &segments.Aws{} }, AZ: func() SegmentWriter { return &segments.Az{} }, AZD: func() SegmentWriter { return &segments.Azd{} }, AZFUNC: func() SegmentWriter { return &segments.AzFunc{} }, BATTERY: func() SegmentWriter { return &segments.Battery{} }, BAZEL: func() SegmentWriter { return &segments.Bazel{} }, BREWFATHER: func() SegmentWriter { return &segments.Brewfather{} }, BUF: func() SegmentWriter { return &segments.Buf{} }, BUN: func() SegmentWriter { return &segments.Bun{} }, CARBONINTENSITY: func() SegmentWriter { return &segments.CarbonIntensity{} }, CDS: func() SegmentWriter { return &segments.Cds{} }, CF: func() SegmentWriter { return &segments.Cf{} }, CFTARGET: func() SegmentWriter { return &segments.CfTarget{} }, CLAUDE: func() SegmentWriter { return &segments.Claude{} }, CLOJURE: func() SegmentWriter { return &segments.Clojure{} }, CMAKE: func() SegmentWriter { return &segments.Cmake{} }, CONNECTION: func() SegmentWriter { return &segments.Connection{} }, COPILOT: func() SegmentWriter { return &segments.Copilot{} }, CRYSTAL: func() SegmentWriter { return &segments.Crystal{} }, DART: func() SegmentWriter { return &segments.Dart{} }, DENO: func() SegmentWriter { return &segments.Deno{} }, DOCKER: func() SegmentWriter { return &segments.Docker{} }, DOTNET: func() SegmentWriter { return &segments.Dotnet{} }, ELIXIR: func() SegmentWriter { return &segments.Elixir{} }, EXECUTIONTIME: func() SegmentWriter { return &segments.Executiontime{} }, EXIT: func() SegmentWriter { return &segments.Status{} }, FIREBASE: func() SegmentWriter { return &segments.Firebase{} }, FLUTTER: func() SegmentWriter { return &segments.Flutter{} }, FORTRAN: func() SegmentWriter { return &segments.Fortran{} }, FOSSIL: func() SegmentWriter { return &segments.Fossil{} }, GCP: func() SegmentWriter { return &segments.Gcp{} }, GIT: func() SegmentWriter { return &segments.Git{} }, GITVERSION: func() SegmentWriter { return &segments.GitVersion{} }, GOLANG: func() SegmentWriter { return &segments.Golang{} }, HASKELL: func() SegmentWriter { return &segments.Haskell{} }, HELM: func() SegmentWriter { return &segments.Helm{} }, IPIFY: func() SegmentWriter { return &segments.IPify{} }, JAVA: func() SegmentWriter { return &segments.Java{} }, HTTP: func() SegmentWriter { return &segments.HTTP{} }, JUJUTSU: func() SegmentWriter { return &segments.Jujutsu{} }, JULIA: func() SegmentWriter { return &segments.Julia{} }, KOTLIN: func() SegmentWriter { return &segments.Kotlin{} }, KUBECTL: func() SegmentWriter { return &segments.Kubectl{} }, LASTFM: func() SegmentWriter { return &segments.LastFM{} }, LUA: func() SegmentWriter { return &segments.Lua{} }, MERCURIAL: func() SegmentWriter { return &segments.Mercurial{} }, MOJO: func() SegmentWriter { return &segments.Mojo{} }, MVN: func() SegmentWriter { return &segments.Mvn{} }, NBA: func() SegmentWriter { return &segments.Nba{} }, NBGV: func() SegmentWriter { return &segments.Nbgv{} }, NIGHTSCOUT: func() SegmentWriter { return &segments.Nightscout{} }, NIXSHELL: func() SegmentWriter { return &segments.NixShell{} }, NIM: func() SegmentWriter { return &segments.Nim{} }, NODE: func() SegmentWriter { return &segments.Node{} }, NPM: func() SegmentWriter { return &segments.Npm{} }, NX: func() SegmentWriter { return &segments.Nx{} }, OCAML: func() SegmentWriter { return &segments.OCaml{} }, OS: func() SegmentWriter { return &segments.Os{} }, OWM: func() SegmentWriter { return &segments.Owm{} }, PATH: func() SegmentWriter { return &segments.Path{} }, PERL: func() SegmentWriter { return &segments.Perl{} }, PHP: func() SegmentWriter { return &segments.Php{} }, PLASTIC: func() SegmentWriter { return &segments.Plastic{} }, PNPM: func() SegmentWriter { return &segments.Pnpm{} }, PROJECT: func() SegmentWriter { return &segments.Project{} }, PULUMI: func() SegmentWriter { return &segments.Pulumi{} }, PYTHON: func() SegmentWriter { return &segments.Python{} }, QUASAR: func() SegmentWriter { return &segments.Quasar{} }, R: func() SegmentWriter { return &segments.R{} }, RAMADAN: func() SegmentWriter { return &segments.Ramadan{} }, REACT: func() SegmentWriter { return &segments.React{} }, ROOT: func() SegmentWriter { return &segments.Root{} }, RUBY: func() SegmentWriter { return &segments.Ruby{} }, RUST: func() SegmentWriter { return &segments.Rust{} }, SAPLING: func() SegmentWriter { return &segments.Sapling{} }, SESSION: func() SegmentWriter { return &segments.Session{} }, SHELL: func() SegmentWriter { return &segments.Shell{} }, SITECORE: func() SegmentWriter { return &segments.Sitecore{} }, SPOTIFY: func() SegmentWriter { return &segments.Spotify{} }, STATUS: func() SegmentWriter { return &segments.Status{} }, STRAVA: func() SegmentWriter { return &segments.Strava{} }, SVELTE: func() SegmentWriter { return &segments.Svelte{} }, SVN: func() SegmentWriter { return &segments.Svn{} }, SWIFT: func() SegmentWriter { return &segments.Swift{} }, SYSTEMINFO: func() SegmentWriter { return &segments.SystemInfo{} }, TALOSCTL: func() SegmentWriter { return &segments.TalosCTL{} }, TASKWARRIOR: func() SegmentWriter { return &segments.Taskwarrior{} }, TAURI: func() SegmentWriter { return &segments.Tauri{} }, TERRAFORM: func() SegmentWriter { return &segments.Terraform{} }, TEXT: func() SegmentWriter { return &segments.Text{} }, TIME: func() SegmentWriter { return &segments.Time{} }, TODOIST: func() SegmentWriter { return &segments.Todoist{} }, UI5TOOLING: func() SegmentWriter { return &segments.UI5Tooling{} }, UMBRACO: func() SegmentWriter { return &segments.Umbraco{} }, UNITY: func() SegmentWriter { return &segments.Unity{} }, UPGRADE: func() SegmentWriter { return &segments.Upgrade{} }, V: func() SegmentWriter { return &segments.V{} }, VALA: func() SegmentWriter { return &segments.Vala{} }, WAKATIME: func() SegmentWriter { return &segments.Wakatime{} }, WINGET: func() SegmentWriter { return &segments.WinGet{} }, WINREG: func() SegmentWriter { return &segments.WindowsRegistry{} }, WITHINGS: func() SegmentWriter { return &segments.Withings{} }, XMAKE: func() SegmentWriter { return &segments.XMake{} }, YARN: func() SegmentWriter { return &segments.Yarn{} }, YTM: func() SegmentWriter { return &segments.Ytm{} }, ZIG: func() SegmentWriter { return &segments.Zig{} }, } func (segment *Segment) MapSegmentWithWriter(env runtime.Environment) error { segment.env = env if segment.Options == nil { segment.Options = make(options.Map) } f, ok := Segments[segment.Type] if !ok { return errors.New("unable to map writer") } writer := f() writer.Init(segment.Options, env) segment.writer = writer return nil } ================================================ FILE: src/constants/constants_unix.go ================================================ //go:build !windows package constants const ( DotnetExitCode = 142 ) ================================================ FILE: src/constants/constants_windows.go ================================================ package constants const ( DotnetExitCode = int(0x80008091) ) ================================================ FILE: src/dsc/cli.go ================================================ package dsc import ( "fmt" "os" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/spf13/cobra" ) var ( state string ) type resource interface { Load() Save() Resolve() ToJSON() string Schema() string Apply(schema string) error Test(input string) error } func Command(r resource) *cobra.Command { cmd := &cobra.Command{ Use: "dsc", Short: "Manage Oh My Posh DSC (Desired State Configuration)", Long: "Manage Oh My Posh DSC (Desired State Configuration).", ValidArgs: []string{"get", "set", "test", "schema", "export"}, Run: func(cmd *cobra.Command, args []string) { if len(args) == 0 { _ = cmd.Help() return } env := &runtime.Terminal{} env.Init(&runtime.Flags{}) cache.Init(os.Getenv("POSH_SHELL"), cache.Persist) defer func() { cache.Close() }() var err error switch args[0] { case "get", "export": r.Load() r.Resolve() fmt.Print(r.ToJSON()) case "set": if state == "" { err = newError("please provide a state configuration to set") break } r.Load() err = r.Apply(state) case "schema": fmt.Print(r.Schema()) case "test": if state == "" { err = newError("please provide a state configuration to test") break } r.Load() err = r.Test(state) default: _ = cmd.Help() return } if err != nil { fmt.Println(err.Error()) return } }, } cmd.Flags().StringVar(&state, "state", "", "State configuration to set") return cmd } ================================================ FILE: src/dsc/error.go ================================================ package dsc type Error struct { message string } func (e *Error) Error() string { return `{ "error": "` + e.message + `" }` } func newError(message string) *Error { return &Error{ message: message, } } ================================================ FILE: src/dsc/resource.go ================================================ package dsc import ( "bytes" "encoding/json" "errors" "reflect" "strings" "github.com/invopop/jsonschema" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/log" ) type Resource[T State[T]] struct { States []T `json:"states,omitempty" jsonschema:"title=states,description=The different states of the resource"` } type State[T any] interface { Equal(state T) bool Apply() error Resolve() (T, bool) } func (resource *Resource[T]) Load() { states, ok := cache.Get[[]T](cache.Device, resource.cacheKey()) if !ok { log.Debug("no states found in cache") return } resource.States = states } func (resource *Resource[T]) Save() { cache.Set(cache.Device, resource.cacheKey(), resource.States, cache.INFINITE) } func (resource *Resource[T]) Add(item T) { for _, existingItem := range resource.States { if existingItem.Equal(item) { log.Debug("item already exists") return } } log.Debug("adding item") resource.States = append(resource.States, item) } func (resource *Resource[T]) Resolve() { for _, item := range resource.States { if resolvedItem, ok := item.Resolve(); ok { resource.States = append(resource.States, resolvedItem) } } } func (resource *Resource[T]) Apply(schema string) error { log.Debug("applying items") err := json.Unmarshal([]byte(schema), resource) if err != nil { return newError(err.Error()) } // TODO: validate if we need to filter out States // which are already available in the cache (and thus set) for _, item := range resource.States { if applyErr := item.Apply(); applyErr != nil { log.Error(applyErr) err = errors.Join(err, applyErr) } } log.Debug("items applied") resource.Save() if err != nil { return newError(err.Error()) } return nil } func (resource *Resource[T]) Test(_ string) error { return newError("test functionality not implemented") } func (resource *Resource[T]) Schema() string { reflector := jsonschema.Reflector{ ExpandedStruct: true, DoNotReference: true, } schema := reflector.Reflect(resource) schema.ID = jsonschema.ID(resource.getItemTypeName()) schema.Properties.Delete("$schema") schemaJSON, _ := json.MarshalIndent(schema, "", " ") return string(schemaJSON) } func (resource *Resource[T]) getItemTypeName() string { var zero T t := reflect.TypeOf(zero) if t.Kind() == reflect.Pointer { return strings.ToLower(t.Elem().Name()) } return strings.ToLower(t.Name()) } func (resource *Resource[T]) cacheKey() string { return "DSC_" + strings.ToUpper(resource.getItemTypeName()) } func (resource *Resource[T]) ToJSON() string { var result bytes.Buffer jsonEncoder := json.NewEncoder(&result) jsonEncoder.SetEscapeHTML(false) _ = jsonEncoder.Encode(resource) return result.String() } ================================================ FILE: src/generics/convert.go ================================================ package generics import ( "errors" "strconv" ) type Numeric interface { ~int | ~int64 | ~uint64 | ~float64 } func toNumeric[T Numeric](value any) (T, error) { switch v := value.(type) { case string: parsed, err := strconv.ParseFloat(v, 64) if err == nil { return T(parsed), nil } return T(0), err case int: return T(v), nil case int64: return T(v), nil case uint64: return T(v), nil case float64: return T(v), nil case bool: if v { return T(1), nil } return T(0), nil default: return T(0), errors.New("invalid numeric type") } } func TryParseInt[T ~int | ~int64](value any) (T, error) { return toNumeric[T](value) } func TryParseFloat[T ~float64](value any) (T, error) { return toNumeric[T](value) } func ToInt[T ~int | ~int64](value any) T { result, err := toNumeric[T](value) if err != nil { return T(0) } return result } ================================================ FILE: src/generics/pool.go ================================================ package generics import "sync" type Pool[T any] struct { pool sync.Pool new func() T } func NewPool[T any](newFunc func() T) *Pool[T] { return &Pool[T]{ pool: sync.Pool{ New: func() any { return newFunc() }, }, new: newFunc, } } func (p *Pool[T]) Get() T { return p.pool.Get().(T) } func (p *Pool[T]) Put(item T) { p.pool.Put(item) } ================================================ FILE: src/generics/slices.go ================================================ package generics import "fmt" // ParseStringSlice converts any slice to a string slice func ParseStringSlice(param any) []string { return parseSlice(param, func(v any) string { return fmt.Sprint(v) }) } // parseSlice converts any slice type to a typed slice using a converter function func parseSlice[T any](param any, converter func(any) T) []T { switch v := param.(type) { case []any: if len(v) == 0 { return []T{} } result := make([]T, len(v)) for i, item := range v { result[i] = converter(item) } return result case []T: return v default: return []T{} } } ================================================ FILE: src/go.mod ================================================ module github.com/jandedobbeleer/oh-my-posh/src go 1.26.0 require ( github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c github.com/Masterminds/sprig/v3 v3.3.0 github.com/alecthomas/assert v1.0.0 github.com/alecthomas/colour v0.1.0 // indirect github.com/alecthomas/repr v0.5.2 // indirect github.com/esimov/stackblur-go v1.1.1 github.com/fogleman/gg v1.3.0 github.com/google/uuid v1.6.0 github.com/gookit/color v1.6.0 github.com/huandu/xstrings v1.5.0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/sergi/go-diff v1.4.0 // indirect github.com/stretchr/objx v0.5.3 // indirect github.com/stretchr/testify v1.11.1 github.com/wayneashleyberry/terminal-dimensions v1.1.0 golang.org/x/crypto v0.48.0 // indirect golang.org/x/image v0.37.0 golang.org/x/sys v0.42.0 golang.org/x/text v0.35.0 gopkg.in/ini.v1 v1.67.1 ) require ( github.com/ConradIrwin/font v0.2.1 github.com/charmbracelet/bubbles v1.0.0 github.com/charmbracelet/bubbletea v1.3.10 github.com/charmbracelet/lipgloss v1.1.0 github.com/gookit/goutil v0.7.4 github.com/hashicorp/hcl/v2 v2.24.0 github.com/invopop/jsonschema v0.13.0 github.com/mattn/go-runewidth v0.0.21 github.com/pelletier/go-toml/v2 v2.2.4 github.com/shirou/gopsutil/v4 v4.26.2 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 go.yaml.in/yaml/v3 v3.0.4 golang.org/x/mod v0.34.0 ) require ( github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/tklauser/go-sysconf v0.3.16 // indirect github.com/tklauser/numcpus v0.11.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect ) require ( dario.cat/mergo v1.0.2 // indirect dmitri.shuralyov.com/font/woff2 v0.0.0-20180220214647-957792cbbdab // indirect github.com/agext/levenshtein v1.2.3 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/buger/jsonparser v1.1.2 // indirect github.com/charmbracelet/colorprofile v0.4.1 // indirect github.com/charmbracelet/harmonica v0.2.0 // indirect github.com/charmbracelet/x/ansi v0.11.6 // indirect github.com/charmbracelet/x/cellbuf v0.0.15 // indirect github.com/charmbracelet/x/term v0.2.2 // indirect github.com/clipperhouse/displaywidth v0.10.0 // indirect github.com/clipperhouse/uax29/v2 v2.6.0 // indirect github.com/dsnet/compress v0.0.1 // indirect github.com/ebitengine/purego v0.10.0 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mailru/easyjson v0.9.1 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.16.0 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/sahilm/fuzzy v0.1.1 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/zclconf/go-cty v1.17.0 // indirect golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/tools v0.42.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) replace github.com/atotto/clipboard v0.1.4 => github.com/jandedobbeleer/clipboard v0.1.4-1 ================================================ FILE: src/go.sum ================================================ dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= dmitri.shuralyov.com/font/woff2 v0.0.0-20180220214647-957792cbbdab h1:Ew70NL+wL6v9looOiJJthlqA41VzoJS+q9AyjHJe6/g= dmitri.shuralyov.com/font/woff2 v0.0.0-20180220214647-957792cbbdab/go.mod h1:FvHgTMJanm43G7B3MVSjS/jim5ytVqAJNAOpRhnuHJc= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/ConradIrwin/font v0.2.1 h1:D4tWi7zyRAdVKOtOys5960HnAAfUSRx/syaf+J9JqlI= github.com/ConradIrwin/font v0.2.1/go.mod h1:krTLO7JWu6g8RMxG8sl+T1Hf8W93XQacBKJmqFZ2MFY= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/alecthomas/assert v1.0.0 h1:3XmGh/PSuLzDbK3W2gUbRXwgW5lqPkuqvRgeQ30FI5o= github.com/alecthomas/assert v1.0.0/go.mod h1:va/d2JC+M7F6s+80kl/R3G7FUiW6JzUO+hPhLyJ36ZY= github.com/alecthomas/colour v0.1.0 h1:nOE9rJm6dsZ66RGWYSFrXw461ZIt9A6+nHgL7FRrDUk= github.com/alecthomas/colour v0.1.0/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY= github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk= github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc= github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E= github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk= github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk= github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= github.com/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g= github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs= github.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoKXuhpos= github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q= github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo= github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/esimov/stackblur-go v1.1.1 h1:jZhuCbyFBp34SxkMwCuuNQ+d42w+CE/WOlcJLOlPEag= github.com/esimov/stackblur-go v1.1.1/go.mod h1:m0T0MjHYbo4Lib/R33XDUMbLBwyGf1/K48ZdqtXUYDA= github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gookit/assert v0.1.1 h1:lh3GcawXe/p+cU7ESTZ5Ui3Sm/x8JWpIis4/1aF0mY0= github.com/gookit/assert v0.1.1/go.mod h1:jS5bmIVQZTIwk42uXl4lyj4iaaxx32tqH16CFj0VX2E= github.com/gookit/color v1.6.0 h1:JjJXBTk1ETNyqyilJhkTXJYYigHG24TM9Xa2M1xAhRA= github.com/gookit/color v1.6.0/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs= github.com/gookit/goutil v0.7.4 h1:OWgUngToNz+bPlX5aP+EMG31DraEU63uvKMwwT3vseM= github.com/gookit/goutil v0.7.4/go.mod h1:vJS9HXctYTCLtCsZot5L5xF+O1oR17cDYO9R0HxBmnU= github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE= github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/jandedobbeleer/clipboard v0.1.4-1 h1:rJehm5W0a3hvjcxyB3snqLBV4yvMBBc12JyMP7ngNQw= github.com/jandedobbeleer/clipboard v0.1.4-1/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k= github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w= github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/shirou/gopsutil/v4 v4.26.2 h1:X8i6sicvUFih4BmYIGT1m2wwgw2VG9YgrDTi7cIRGUI= github.com/shirou/gopsutil/v4 v4.26.2/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/shurcooL/gofontwoff v0.0.0-20181114050219-180f79e6909d h1:lvCTyBbr36+tqMccdGMwuEU+hjux/zL6xSmf5S9ITaA= github.com/shurcooL/gofontwoff v0.0.0-20181114050219-180f79e6909d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= github.com/wayneashleyberry/terminal-dimensions v1.1.0 h1:EB7cIzBdsOzAgmhTUtTTQXBByuPheP/Zv1zL2BRPY6g= github.com/wayneashleyberry/terminal-dimensions v1.1.0/go.mod h1:2lc/0eWCObmhRczn2SdGSQtgBooLUzIotkkEGXqghyg= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zclconf/go-cty v1.17.0 h1:seZvECve6XX4tmnvRzWtJNHdscMtYEx5R7bnnVyd/d0= github.com/zclconf/go-cty v1.17.0/go.mod h1:wqFzcImaLTI6A5HfsRwB0nj5n0MRZFwmey8YoFPPs3U= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 h1:1UoZQm6f0P/ZO0w1Ri+f+ifG/gXhegadRdwBIXEFWDo= golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= golang.org/x/image v0.37.0 h1:ZiRjArKI8GwxZOoEtUfhrBtaCN+4b/7709dlT6SSnQA= golang.org/x/image v0.37.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY= golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/ini.v1 v1.67.1 h1:tVBILHy0R6e4wkYOn3XmiITt/hEVH4TFMYvAX2Ytz6k= gopkg.in/ini.v1 v1.67.1/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: src/log/log.go ================================================ package log import ( "fmt" "path/filepath" "runtime" "strings" "time" ) var ( enabled bool raw bool log strings.Builder ) func Enable(plain bool) { enabled = true raw = plain Debugf("logging enabled, raw mode: %t", plain) } func Trace(start time.Time, args ...string) { if !enabled { return } elapsed := time.Since(start) fn, _ := funcSpec() // Color-code elapsed time based on duration var coloredElapsed Text ms := elapsed.Milliseconds() switch { case ms < 1: coloredElapsed = Text(elapsed.String()).Green().Plain() case ms >= 1 && ms < 10: coloredElapsed = Text(elapsed.String()).Yellow().Plain() case ms >= 10 && ms < 100: coloredElapsed = Text(elapsed.String()).Orange().Plain() default: // >= 100ms coloredElapsed = Text(elapsed.String()).Red().Plain() } header := fmt.Sprintf("%s(%s) - %s", fn, strings.Join(args, " "), coloredElapsed) printLn(trace, header) } func Debug(message ...string) { if !enabled { return } fn, line := funcSpec() header := fmt.Sprintf("%s:%d", fn, line) printLn(debug, header, strings.Join(message, " ")) } func Debugf(format string, args ...any) { if !enabled { return } message := fmt.Sprintf(format, args...) Debug(message) } func Error(err error) { if !enabled { return } fn, line := funcSpec() header := fmt.Sprintf("%s:%d", fn, line) printLn(bug, header, err.Error()) } func Errorf(format string, args ...any) { if !enabled { return } Error(fmt.Errorf(format, args...)) } func String() string { return log.String() } func funcSpec() (string, int) { pcs := make([]uintptr, 4) n := runtime.Callers(3, pcs) if n == 0 { return "", 0 } frames := runtime.CallersFrames(pcs[:n]) var frame runtime.Frame more := true // Loop through frames until we're out of log.go for more { frame, more = frames.Next() if strings.Contains(frame.File, "log.go") { continue } // Found first non-log.go frame fn := frame.Function fn = fn[strings.LastIndex(fn, ".")+1:] file := filepath.Base(frame.File) if strings.HasPrefix(fn, "func") { return file, frame.Line } return fmt.Sprintf("%s:%s", file, fn), frame.Line } return "", 0 } ================================================ FILE: src/log/print.go ================================================ package log import ( "fmt" "strings" "time" ) type logType byte const ( debug logType = 1 << iota bug trace ) type Text string func (t Text) Green() Text { if raw { return t } return "\x1b[38;2;191;207;240m" + t } func (t Text) Red() Text { if raw { return t } return "\x1b[38;2;253;122;140m" + t } func (t Text) Purple() Text { if raw { return t } return "\x1b[38;2;204;137;214m" + t } func (t Text) Yellow() Text { if raw { return t } return "\x1b[38;2;156;231;201m" + t } func (t Text) Orange() Text { if raw { return t } return "\x1b[38;2;253;184;109m" + t } func (t Text) Bold() Text { if raw { return t } return "\x1b[1m" + t } func (t Text) Plain() Text { if raw { return t } return t + "\033[0m" } func (t Text) String() string { return string(t) } func printLn(lt logType, args ...string) { if len(args) == 0 { return } var str Text switch lt { case debug: str = Text("[DEBUG] ").Green() case bug: str = Text("[ERROR] ").Red() case trace: str = Text("[TRACE] ").Purple() } // timestamp 156, 231, 201 str += Text(time.Now().Format("15:04:05.000") + " ").Yellow().Plain() str += Text(args[0]) str += parseArgs(args...) log.WriteString(str.String()) } func parseArgs(args ...string) Text { if len(args) == 1 { return "\n" } // display empty return values as NO DATA if args[1] == "" { text := Text(" \u2192").Yellow() text += Text(" NO DATA\n").Red().Plain() return text } // print a single line for single output splitted := strings.Split(args[1], "\n") if len(splitted) == 1 { text := Text(" \u2192").Yellow().Plain() return Text(fmt.Sprintf("%s %s\n", text, args[1])) } // indent multiline output with 4 spaces var str Text str += Text(" \u2193\n").Yellow().Plain() for _, line := range splitted { str += Text(fmt.Sprintf(" %s\n", line)) } return str } ================================================ FILE: src/main.go ================================================ package main import ( "github.com/jandedobbeleer/oh-my-posh/src/cli" ) func main() { cli.Execute() } ================================================ FILE: src/main_test.go ================================================ package main import ( "bytes" "fmt" "testing" "github.com/jandedobbeleer/oh-my-posh/src/cli" "github.com/jandedobbeleer/oh-my-posh/src/prompt" ) func BenchmarkInit(b *testing.B) { cmd := cli.RootCmd // needs to be a non-existing file as we panic otherwise cmd.SetArgs([]string{"init", "fish", "--print", "--silent"}) out := bytes.NewBufferString("") cmd.SetOut(out) for b.Loop() { _ = cmd.Execute() } } func BenchmarkPrimary(b *testing.B) { cmd := cli.RootCmd // needs to be a non-existing file as we panic otherwise cmd.SetArgs([]string{"print", prompt.PRIMARY, "--pwd", "/Users/jan/Code/oh-my-posh/src", "--shell", "fish", "--silent"}) out := bytes.NewBufferString("") cmd.SetOut(out) for b.Loop() { _ = cmd.Execute() } fmt.Println("") } ================================================ FILE: src/maps/concurrent.go ================================================ package maps import ( "fmt" "sync" "github.com/jandedobbeleer/oh-my-posh/src/log" ) func NewConcurrent[V any]() *Concurrent[V] { return &Concurrent[V]{} } // Concurrent is a generic type-safe concurrent map type Concurrent[V any] struct { m sync.Map } func (cm *Concurrent[V]) Set(key string, value V) { cm.m.Store(key, value) } func (cm *Concurrent[V]) Get(key string) (V, bool) { val, ok := cm.m.Load(key) if !ok { var zero V return zero, false } return val.(V), true } func (cm *Concurrent[V]) MustGet(key string) V { val, ok := cm.m.Load(key) if !ok { log.Error(fmt.Errorf("key %s not found", key)) var zero V return zero } return val.(V) } func (cm *Concurrent[V]) Delete(key string) { cm.m.Delete(key) } func (cm *Concurrent[V]) Contains(key string) bool { _, ok := cm.m.Load(key) return ok } func (cm *Concurrent[V]) ToSimple() Simple[V] { result := make(Simple[V]) cm.m.Range(func(key, value any) bool { if value == nil { return true } result[key.(string)] = value.(V) return true }) return result } ================================================ FILE: src/maps/config.go ================================================ package maps import ( "encoding/gob" ) func init() { gob.Register(&Config{}) gob.Register(&Map{}) } type Config struct { UserName *Map `json:"user_name,omitempty" toml:"user_name,omitempty" yaml:"user_name,omitempty"` HostName *Map `json:"host_name,omitempty" toml:"host_name,omitempty" yaml:"host_name,omitempty"` ShellName *Map `json:"shell_name,omitempty" toml:"shell_name,omitempty" yaml:"shell_name,omitempty"` } func (c *Config) GetUserName(key string) string { if c == nil || c.UserName == nil { return key } return c.UserName.Get(key) } func (c *Config) GetHostName(key string) string { if c == nil || c.HostName == nil { return key } return c.HostName.Get(key) } func (c *Config) GetShellName(key string) string { if c == nil || c.ShellName == nil { return key } return c.ShellName.Get(key) } type Map map[string]string func (m *Map) Get(key string) string { if m == nil { return key } if value, ok := (*m)[key]; ok { return value } return key } ================================================ FILE: src/maps/simple.go ================================================ package maps // Simple is a generic map type that can be specialized for different value types type Simple[V any] map[string]V func (m Simple[V]) ToConcurrent() *Concurrent[V] { cm := NewConcurrent[V]() for k, v := range m { cm.Set(k, v) } return cm } ================================================ FILE: src/metadata.json ================================================ { "Endpoint": "https://weu.codesigning.azure.net", "CodeSigningAccountName": "oh-my-posh", "CertificateProfileName": "oh-my-posh", "ExcludeCredentials": [ "AzureCliCredential", "AzurePowerShellCredential", "ManagedIdentityCredential", "SharedTokenCacheCredential", "VisualStudioCredential", "VisualStudioCodeCredential", "InteractiveBrowserCredential" ] } ================================================ FILE: src/prompt/debug.go ================================================ package prompt import ( "fmt" "time" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/config" "github.com/jandedobbeleer/oh-my-posh/src/log" ) // debug will loop through your config file and output the timings for each segments func (e *Engine) PrintDebug(startTime time.Time, version string) string { e.write(fmt.Sprintf("\n%s %s\n", log.Text("Version:").Green().Bold().Plain(), version)) sh := e.Env.Shell() shellVersion := e.Env.Getenv("POSH_SHELL_VERSION") if len(shellVersion) != 0 { sh += fmt.Sprintf(" (%s)", shellVersion) } e.write(fmt.Sprintf("\n%s %s\n", log.Text("Shell:").Green().Bold().Plain(), sh)) // console title timing titleStartTime := time.Now() log.Debug("segment: Title") consoleTitle := &config.Segment{ Alias: "ConsoleTitle", NameLength: 12, Enabled: len(e.Config.ConsoleTitleTemplate) > 0, Duration: time.Since(titleStartTime), Type: config.TEXT, } _ = consoleTitle.MapSegmentWithWriter(e.Env) consoleTitle.SetText(e.getTitleTemplateText()) largestSegmentNameLength := consoleTitle.NameLength // render prompt e.write(log.Text("\nPrompt:\n\n").Green().Bold().Plain().String()) e.write(e.Primary()) e.write(log.Text("\n\nSegments:\n\n").Green().Bold().Plain().String()) var segments []*config.Segment segments = append(segments, consoleTitle) for _, block := range e.Config.Blocks { for _, segment := range block.Segments { segments = append(segments, segment) if segment.NameLength > largestSegmentNameLength { largestSegmentNameLength = segment.NameLength } } } // 22 is the color for false/true and 7 is the reset color largestSegmentNameLength += 22 + 7 for _, segment := range segments { duration := segment.Duration.Milliseconds() var active log.Text if segment.Enabled { active = log.Text("true").Yellow() } else { active = log.Text("false").Purple() } segmentName := fmt.Sprintf("%s(%s)", segment.Name(), active.Plain()) e.write(fmt.Sprintf("%-*s - %3d ms\n", largestSegmentNameLength, segmentName, duration)) } e.write(fmt.Sprintf("\n%s %s\n", log.Text("Run duration:").Green().Bold().Plain(), time.Since(startTime))) e.write(fmt.Sprintf("\n%s %s\n", log.Text("Cache path:").Green().Bold().Plain(), cache.Path())) cfg := e.Config.Source if cfg == "" { cfg = "no --config set, using default built-in configuration" } e.write(fmt.Sprintf("\n%s %s\n", log.Text("Config path:").Green().Bold().Plain(), cfg)) e.write(log.Text("\nLogs:\n\n").Green().Bold().Plain().String()) e.write(e.Env.Logs()) return e.string() } ================================================ FILE: src/prompt/engine.go ================================================ package prompt import ( "strings" "sync" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/color" "github.com/jandedobbeleer/oh-my-posh/src/config" "github.com/jandedobbeleer/oh-my-posh/src/log" "github.com/jandedobbeleer/oh-my-posh/src/regex" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/shell" "github.com/jandedobbeleer/oh-my-posh/src/template" "github.com/jandedobbeleer/oh-my-posh/src/terminal" ) var cycle *color.Cycle = &color.Cycle{} type Engine struct { Env runtime.Environment streamingResults chan *config.Segment Config *config.Config activeSegment *config.Segment previousActiveSegment *config.Segment pendingSegments sync.Map rprompt string Overflow config.Overflow prompt strings.Builder allBlocks []*config.Block currentLineLength int Padding int rpromptLength int Plain bool forceRender bool } const ( PRIMARY = "primary" TRANSIENT = "transient" DEBUG = "debug" SECONDARY = "secondary" RIGHT = "right" TOOLTIP = "tooltip" VALID = "valid" ERROR = "error" PREVIEW = "preview" ) func (e *Engine) write(txt string) { // Grow capacity proactively if needed if e.prompt.Cap() < e.prompt.Len()+len(txt) { e.prompt.Grow(len(txt) * 2) // Grow by double the needed size to reduce future allocations } e.prompt.WriteString(txt) } func (e *Engine) string() string { txt := e.prompt.String() e.prompt.Reset() return txt } func (e *Engine) canWriteRightBlock(length int, rprompt bool) (int, bool) { if rprompt && (e.rprompt == "") { return 0, false } consoleWidth, err := e.Env.TerminalWidth() if err != nil || consoleWidth == 0 { return 0, false } availableSpace := consoleWidth - e.currentLineLength // spanning multiple lines if availableSpace < 0 { overflow := e.currentLineLength % consoleWidth availableSpace = consoleWidth - overflow } availableSpace -= length promptBreathingRoom := 5 if rprompt { promptBreathingRoom = 30 } canWrite := availableSpace >= promptBreathingRoom // reset the available space when we can't write so we can fill the line if !canWrite { availableSpace = consoleWidth - length } return availableSpace, canWrite } func (e *Engine) pwd() { // only print when relevant if e.Config.PWD == "" { return } // only print when supported sh := e.Env.Shell() if sh == shell.ELVISH || sh == shell.XONSH { return } pwd := e.Env.Pwd() if e.Env.IsCygwin() { pwd = strings.ReplaceAll(pwd, `\`, `/`) } // Allow template logic to define when to enable the PWD (when supported) pwdType, err := template.Render(e.Config.PWD, nil) if err != nil || pwdType == "" { return } // Convert to Windows path when in WSL if e.Env.IsWsl() { pwd = e.Env.ConvertToWindowsPath(pwd) } user := e.Env.User() host, _ := e.Env.Host() e.write(terminal.Pwd(pwdType, user, host, pwd)) } func (e *Engine) getNewline() string { newline := "\n" if e.Plain || e.Env.Flags().Debug { return newline } // Warp terminal will remove a newline character ('\n') from the prompt, so we hack it in. if e.isWarp() { return terminal.LineBreak() } return newline } func (e *Engine) writeNewline() { defer func() { e.currentLineLength = 0 }() e.write(e.getNewline()) } func (e *Engine) isWarp() bool { return terminal.Program == terminal.Warp } func (e *Engine) isIterm() bool { return terminal.Program == terminal.ITerm } func (e *Engine) shouldFill(filler string, padLength int) (string, bool) { if filler == "" { log.Debug("no filler specified") return "", false } e.Padding = padLength defer func() { e.Padding = 0 }() var err error if filler, err = template.Render(filler, e); err != nil { return "", false } // allow for easy color overrides and templates terminal.SetColors("default", "default") terminal.Write("", "", filler) filler, lenFiller := terminal.String() if lenFiller == 0 { log.Debug("filler has no length") return "", false } repeat := padLength / lenFiller unfilled := padLength % lenFiller txt := strings.Repeat(filler, repeat) + strings.Repeat(" ", unfilled) log.Debug("filling with", txt) return txt, true } func (e *Engine) getTitleTemplateText() string { if txt, err := template.Render(e.Config.ConsoleTitleTemplate, nil); err == nil { return txt } return "" } func (e *Engine) renderBlock(block *config.Block, cancelNewline bool) bool { blockText, length := e.writeBlockSegments(block) // do not print anything when we don't have any text unless forced if !block.Force && length == 0 { return false } return e.writeBlock(block, blockText, length, cancelNewline) } // writeBlock handles the common logic for writing a block to the prompt func (e *Engine) writeBlock(block *config.Block, blockText string, length int, cancelNewline bool) bool { defer func() { e.applyPowerShellBleedPatch() }() // do not print a newline to avoid a leading space // when we're printing the first primary prompt in // the shell if block.Newline && !cancelNewline { e.writeNewline() } switch block.Type { case config.Prompt: if block.Alignment == config.Left { e.currentLineLength += length e.write(blockText) return true } if block.Alignment != config.Right { return false } space, OK := e.canWriteRightBlock(length, false) // we can't print the right block as there's not enough room available if !OK { e.Overflow = block.Overflow switch e.Overflow { case config.Break: e.writeNewline() case config.Hide: // make sure to fill if needed if padText, OK := e.shouldFill(block.Filler, space+length-e.currentLineLength); OK { e.write(padText) } e.currentLineLength = 0 return true } } defer func() { e.currentLineLength = 0 e.Overflow = "" }() // validate if we have a filler and fill if needed if padText, OK := e.shouldFill(block.Filler, space); OK { e.write(padText) e.write(blockText) return true } if space > 0 { e.write(strings.Repeat(" ", space)) } e.write(blockText) case config.RPrompt: e.rprompt = blockText e.rpromptLength = length } return true } // renderBlockFromCache re-renders a block using existing segment data without re-execution func (e *Engine) renderBlockFromCache(block *config.Block, cancelNewline bool) bool { // Re-render all segments in the block for segmentIndex, segment := range block.Segments { // Allow pending segments to render (they show "..." text) if !segment.Pending && !segment.Enabled && segment.ResolveStyle() != config.Accordion { continue } // Render segment text (will use pending state if still pending) if !segment.Render(segmentIndex, e.forceRender) { continue } if colors, newCycle := cycle.Loop(); colors != nil { cycle = &newCycle segment.Foreground = colors.Foreground segment.Background = colors.Background } if terminal.Len() == 0 && len(block.LeadingDiamond) > 0 { segment.LeadingDiamond = block.LeadingDiamond } e.setActiveSegment(segment) e.renderActiveSegment() } if e.activeSegment != nil && len(block.TrailingDiamond) > 0 { e.activeSegment.TrailingDiamond = block.TrailingDiamond } e.writeSeparator(true) e.activeSegment = nil e.previousActiveSegment = nil blockText, length := terminal.String() // do not print anything when we don't have any text unless forced if !block.Force && length == 0 { return false } return e.writeBlock(block, blockText, length, cancelNewline) } func (e *Engine) applyPowerShellBleedPatch() { // when in PowerShell, we need to clear the line after the prompt // to avoid the background being printed on the next line // when at the end of the buffer. // See https://github.com/JanDeDobbeleer/oh-my-posh/issues/65 if e.Env.Shell() != shell.PWSH { return } // only do this when enabled if !e.Config.PatchPwshBleed { return } e.write(terminal.ClearAfter()) } func (e *Engine) setActiveSegment(segment *config.Segment) { e.activeSegment = segment terminal.Interactive = segment.Interactive terminal.SetColors(segment.ResolveBackground(), segment.ResolveForeground()) } func (e *Engine) renderActiveSegment() { e.writeSeparator(false) switch e.activeSegment.ResolveStyle() { case config.Plain, config.Powerline: terminal.Write(color.Background, color.Foreground, e.activeSegment.Text()) case config.Diamond: background := color.Transparent if e.previousActiveSegment != nil && e.previousActiveSegment.HasEmptyDiamondAtEnd() { background = e.previousActiveSegment.ResolveBackground() } terminal.Write(background, color.Background, e.activeSegment.LeadingDiamond) terminal.Write(color.Background, color.Foreground, e.activeSegment.Text()) case config.Accordion: // Render accordion segments if enabled OR pending (pending shows "..." text) if e.activeSegment.Enabled || e.activeSegment.Pending { terminal.Write(color.Background, color.Foreground, e.activeSegment.Text()) } } e.previousActiveSegment = e.activeSegment terminal.SetParentColors(e.previousActiveSegment.ResolveBackground(), e.previousActiveSegment.ResolveForeground()) } func (e *Engine) writeSeparator(final bool) { if e.activeSegment == nil { return } isCurrentDiamond := e.activeSegment.ResolveStyle() == config.Diamond if final && isCurrentDiamond { terminal.Write(color.Transparent, color.Background, e.activeSegment.TrailingDiamond) return } isPreviousDiamond := e.previousActiveSegment != nil && e.previousActiveSegment.ResolveStyle() == config.Diamond if isPreviousDiamond { e.adjustTrailingDiamondColorOverrides() } if isPreviousDiamond && isCurrentDiamond && e.activeSegment.LeadingDiamond == "" { terminal.Write(color.Background, color.ParentBackground, e.previousActiveSegment.TrailingDiamond) return } if isPreviousDiamond && len(e.previousActiveSegment.TrailingDiamond) > 0 { terminal.Write(color.Transparent, color.ParentBackground, e.previousActiveSegment.TrailingDiamond) } isPowerline := e.activeSegment.IsPowerline() shouldOverridePowerlineLeadingSymbol := func() bool { if !isPowerline { return false } if isPowerline && e.activeSegment.LeadingPowerlineSymbol == "" { return false } if e.previousActiveSegment != nil && e.previousActiveSegment.IsPowerline() { return false } return true } if shouldOverridePowerlineLeadingSymbol() { terminal.Write(color.Transparent, color.Background, e.activeSegment.LeadingPowerlineSymbol) return } resolvePowerlineSymbol := func() string { if isPowerline { return e.activeSegment.PowerlineSymbol } if e.previousActiveSegment != nil && e.previousActiveSegment.IsPowerline() { return e.previousActiveSegment.PowerlineSymbol } return "" } symbol := resolvePowerlineSymbol() if symbol == "" { return } bgColor := color.Background if final || !isPowerline { bgColor = color.Transparent } if e.activeSegment.ResolveStyle() == config.Diamond && e.activeSegment.LeadingDiamond == "" { bgColor = color.Background } if e.activeSegment.InvertPowerline || (e.previousActiveSegment != nil && e.previousActiveSegment.InvertPowerline) { terminal.Write(e.getPowerlineColor(), bgColor, symbol) return } terminal.Write(bgColor, e.getPowerlineColor(), symbol) } func (e *Engine) getPowerlineColor() color.Ansi { if e.previousActiveSegment == nil { return color.Transparent } if e.previousActiveSegment.ResolveStyle() == config.Diamond && e.previousActiveSegment.TrailingDiamond == "" { return e.previousActiveSegment.ResolveBackground() } if e.activeSegment.ResolveStyle() == config.Diamond && e.activeSegment.LeadingDiamond == "" { return e.previousActiveSegment.ResolveBackground() } if !e.previousActiveSegment.IsPowerline() { return color.Transparent } return e.previousActiveSegment.ResolveBackground() } func (e *Engine) adjustTrailingDiamondColorOverrides() { // as we now already adjusted the activeSegment, we need to change the value // of background and foreground to parentBackground and parentForeground // this will still break when using parentBackground and parentForeground as keywords // in a trailing diamond, but let's fix that when it happens as it requires either a rewrite // of the logic for diamonds or storing grandparents as well like one happy family. if e.previousActiveSegment == nil || e.previousActiveSegment.TrailingDiamond == "" { return } trailingDiamond := e.previousActiveSegment.TrailingDiamond // Optimize: check both conditions in a single pass hasBg := strings.Contains(trailingDiamond, string(color.Background)) hasFg := strings.Contains(trailingDiamond, string(color.Foreground)) if !hasBg && !hasFg { return } match := regex.FindNamedRegexMatch(terminal.AnchorRegex, trailingDiamond) if len(match) == 0 { return } adjustOverride := func(anchor string, override color.Ansi) { newOverride := override switch override { //nolint:exhaustive case color.Foreground: newOverride = color.ParentForeground case color.Background: newOverride = color.ParentBackground } if override == newOverride { return } newAnchor := strings.Replace(match[terminal.ANCHOR], string(override), string(newOverride), 1) e.previousActiveSegment.TrailingDiamond = strings.Replace(e.previousActiveSegment.TrailingDiamond, anchor, newAnchor, 1) } if len(match[terminal.BG]) > 0 { adjustOverride(match[terminal.ANCHOR], color.Ansi(match[terminal.BG])) } if len(match[terminal.FG]) > 0 { adjustOverride(match[terminal.ANCHOR], color.Ansi(match[terminal.FG])) } } func (e *Engine) rectifyTerminalWidth(diff int) { // Since the terminal width may not be given by the CLI flag, we should always call this here. _, err := e.Env.TerminalWidth() if err != nil { // Skip when we're unable to determine the terminal width. return } e.Env.Flags().TerminalWidth += diff } // New returns a prompt engine initialized with the // given configuration options, and is ready to print any // of the prompt components. func New(flags *runtime.Flags) *Engine { env := &runtime.Terminal{} env.Init(flags) reload, _ := cache.Get[bool](cache.Device, config.RELOAD) cfg := config.Get(flags.ConfigPath, reload) template.Init(env, cfg.Var, cfg.Maps) flags.HasExtra = cfg.DebugPrompt != nil || cfg.SecondaryPrompt != nil || cfg.TransientPrompt != nil || cfg.ValidLine != nil || cfg.ErrorLine != nil // when we print using https://github.com/akinomyoga/ble.sh, this needs to be unescaped for certain prompts sh := env.Shell() if sh == shell.BASH && !flags.Escape { sh = shell.GENERIC } terminal.Init(sh) terminal.BackgroundColor = cfg.TerminalBackground.ResolveTemplate() terminal.Colors = cfg.MakeColors(env) terminal.Plain = flags.Plain eng := &Engine{ Config: cfg, Env: env, Plain: flags.Plain, forceRender: flags.Force || len(env.Getenv("POSH_FORCE_RENDER")) > 0, prompt: strings.Builder{}, } // Pre-allocate prompt builder capacity to reduce allocations during rendering eng.prompt.Grow(512) // Start with 512 bytes capacity, will grow as needed switch env.Shell() { case shell.XONSH: // In Xonsh, the behavior of wrapping at the end of a prompt line is inconsistent across different operating systems. // On Windows, it wraps before the last cell on the terminal screen, that is, the last cell is never available for a prompt line. if env.GOOS() == runtime.WINDOWS { eng.rectifyTerminalWidth(-1) } case shell.ELVISH: // In Elvish, the case is similar to that in Xonsh. // However, on Windows, we have to reduce the terminal width by 1 again to ensure that newlines are displayed correctly. diff := -1 if env.GOOS() == runtime.WINDOWS { diff = -2 } eng.rectifyTerminalWidth(diff) case shell.PWSH: // when in PowerShell, and force patching the bleed bug // we need to reduce the terminal width by 1 so the last // character isn't cut off by the ANSI escape sequences // See https://github.com/JanDeDobbeleer/oh-my-posh/issues/65 if cfg.PatchPwshBleed { eng.rectifyTerminalWidth(-1) } } return eng } ================================================ FILE: src/prompt/engine_test.go ================================================ package prompt import ( "errors" "strings" "testing" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/color" "github.com/jandedobbeleer/oh-my-posh/src/config" "github.com/jandedobbeleer/oh-my-posh/src/maps" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/runtime/mock" "github.com/jandedobbeleer/oh-my-posh/src/shell" "github.com/jandedobbeleer/oh-my-posh/src/template" "github.com/jandedobbeleer/oh-my-posh/src/terminal" "github.com/stretchr/testify/assert" ) func TestCanWriteRPrompt(t *testing.T) { cases := []struct { TerminalWidthError error Case string TerminalWidth int PromptLength int RPromptLength int Expected bool }{ {Case: "Width Error", Expected: false, TerminalWidthError: errors.New("burp")}, {Case: "Terminal > Prompt enabled", Expected: true, TerminalWidth: 200, PromptLength: 100, RPromptLength: 10}, {Case: "Terminal > Prompt enabled edge", Expected: true, TerminalWidth: 200, PromptLength: 100, RPromptLength: 70}, {Case: "Prompt > Terminal enabled", Expected: true, TerminalWidth: 200, PromptLength: 300, RPromptLength: 70}, {Case: "Terminal > Prompt disabled no breathing", Expected: false, TerminalWidth: 200, PromptLength: 100, RPromptLength: 71}, {Case: "Prompt > Terminal disabled no breathing", Expected: false, TerminalWidth: 200, PromptLength: 300, RPromptLength: 80}, {Case: "Prompt > Terminal disabled no room", Expected: true, TerminalWidth: 200, PromptLength: 400, RPromptLength: 80}, } for _, tc := range cases { env := new(mock.Environment) env.On("TerminalWidth").Return(tc.TerminalWidth, tc.TerminalWidthError) engine := &Engine{ Env: env, rpromptLength: tc.RPromptLength, currentLineLength: tc.PromptLength, rprompt: "hello", } _, got := engine.canWriteRightBlock(tc.RPromptLength, true) assert.Equal(t, tc.Expected, got, tc.Case) } } func TestPrintPWD(t *testing.T) { cases := []struct { Case string Expected string Config string Pwd string Shell string Cygwin bool }{ {Case: "Empty PWD"}, {Case: "OSC99", Config: terminal.OSC99, Expected: "\x1b]9;9;pwd\x1b\\"}, {Case: "OSC99 - Elvish", Config: terminal.OSC99, Shell: shell.ELVISH}, {Case: "OSC7", Config: terminal.OSC7, Expected: "\x1b]7;file://host/pwd\x1b\\"}, {Case: "OSC51", Config: terminal.OSC51, Expected: "\x1b]51;Auser@host:pwd\x1b\\"}, {Case: "Template (empty)", Config: "{{ if eq .Shell \"pwsh\" }}osc7{{ end }}"}, {Case: "Template (non empty)", Shell: shell.GENERIC, Config: "{{ if eq .Shell \"shell\" }}osc7{{ end }}", Expected: "\x1b]7;file://host/pwd\x1b\\"}, { Case: "OSC99 Cygwin", Pwd: `C:\Users\user\Documents\GitHub\oh-my-posh`, Config: terminal.OSC99, Cygwin: true, Expected: "\x1b]9;9;C:/Users/user/Documents/GitHub/oh-my-posh\x1b\\", }, { Case: "OSC99 Windows", Pwd: `C:\Users\user\Documents\GitHub\oh-my-posh`, Config: terminal.OSC99, Expected: "\x1b]9;9;C:\\Users\\user\\Documents\\GitHub\\oh-my-posh\x1b\\", }, } for _, tc := range cases { env := new(mock.Environment) if tc.Pwd == "" { tc.Pwd = "pwd" } env.On("Pwd").Return(tc.Pwd) env.On("User").Return("user") env.On("Shell").Return(tc.Shell) env.On("IsCygwin").Return(tc.Cygwin) env.On("IsWsl").Return(false) env.On("Host").Return("host", nil) template.Cache = &cache.Template{ SimpleTemplate: cache.SimpleTemplate{ Shell: tc.Shell, }, Segments: maps.NewConcurrent[any](), } template.Init(env, nil, nil) terminal.Init(shell.GENERIC) engine := &Engine{ Env: env, Config: &config.Config{ PWD: tc.Config, }, } engine.pwd() got := engine.string() assert.Equal(t, tc.Expected, got, tc.Case) } } func TestPrintPWDWSL(t *testing.T) { cases := []struct { Case string Expected string Config string Pwd string Shell string WinPath string IsWsl bool }{ { Case: "OSC99 WSL", Pwd: "/home/user/projects", Config: terminal.OSC99, IsWsl: true, WinPath: "//wsl.localhost/Ubuntu/home/user/projects", Expected: "\x1b]9;9;//wsl.localhost/Ubuntu/home/user/projects\x1b\\", }, { Case: "OSC99 Not WSL", Pwd: "/home/user/projects", Config: terminal.OSC99, IsWsl: false, Expected: "\x1b]9;9;/home/user/projects\x1b\\", }, { Case: "OSC7 WSL (with conversion)", Pwd: "/home/user/projects", Config: terminal.OSC7, IsWsl: true, WinPath: "//wsl.localhost/Ubuntu/home/user/projects", Expected: "\x1b]7;file://host///wsl.localhost/Ubuntu/home/user/projects\x1b\\", }, { Case: "OSC51 WSL (with conversion)", Pwd: "/home/user/projects", Config: terminal.OSC51, IsWsl: true, WinPath: "//wsl.localhost/Ubuntu/home/user/projects", Expected: "\x1b]51;Auser@host://wsl.localhost/Ubuntu/home/user/projects\x1b\\", }, } for _, tc := range cases { env := new(mock.Environment) env.On("Pwd").Return(tc.Pwd) env.On("User").Return("user") env.On("Shell").Return(tc.Shell) env.On("IsCygwin").Return(false) env.On("IsWsl").Return(tc.IsWsl) env.On("Host").Return("host", nil) if tc.IsWsl { if tc.WinPath == "" { tc.WinPath = tc.Pwd } env.On("ConvertToWindowsPath", tc.Pwd).Return(tc.WinPath) } template.Cache = &cache.Template{ SimpleTemplate: cache.SimpleTemplate{ Shell: tc.Shell, }, Segments: maps.NewConcurrent[any](), } template.Init(env, nil, nil) terminal.Init(shell.GENERIC) engine := &Engine{ Env: env, Config: &config.Config{ PWD: tc.Config, }, } engine.pwd() got := engine.string() assert.Equal(t, tc.Expected, got, tc.Case) } } func BenchmarkEngineRender(b *testing.B) { for b.Loop() { engineRender() } } func engineRender() { cfg := config.Load("") env := &runtime.Terminal{} env.Init(nil) template.Cache = &cache.Template{ Segments: maps.NewConcurrent[any](), } template.Init(env, nil, nil) terminal.Init(shell.GENERIC) terminal.BackgroundColor = cfg.TerminalBackground.ResolveTemplate() terminal.Colors = cfg.MakeColors(env) engine := &Engine{ Config: cfg, Env: env, } engine.Primary() } func TestGetTitle(t *testing.T) { cases := []struct { Template string User string Cwd string PathSeparator string ShellName string Expected string Root bool }{ { Template: "{{.Env.USERDOMAIN}} :: {{.PWD}}{{if .Root}} :: Admin{{end}} :: {{.Shell}}", Cwd: "C:\\vagrant", PathSeparator: "\\", ShellName: "PowerShell", Root: true, Expected: "\x1b]0;MyCompany :: C:\\vagrant :: Admin :: PowerShell\a", }, { Template: "{{.Folder}}{{if .Root}} :: Admin{{end}} :: {{.Shell}}", Cwd: "C:\\vagrant", PathSeparator: "\\", ShellName: "PowerShell", Expected: "\x1b]0;vagrant :: PowerShell\a", }, { Template: "{{.UserName}}@{{.HostName}}{{if .Root}} :: Admin{{end}} :: {{.Shell}}", Root: true, User: "MyUser", PathSeparator: "\\", ShellName: "PowerShell", Expected: "\x1b]0;MyUser@MyHost :: Admin :: PowerShell\a", }, } for _, tc := range cases { env := new(mock.Environment) env.On("Pwd").Return(tc.Cwd) env.On("Home").Return("/usr/home") env.On("PathSeparator").Return(tc.PathSeparator) env.On("Getenv", "USERDOMAIN").Return("MyCompany") env.On("Shell").Return(tc.ShellName) terminal.Init(shell.GENERIC) template.Cache = &cache.Template{ SimpleTemplate: cache.SimpleTemplate{ Shell: tc.ShellName, UserName: "MyUser", Root: tc.Root, HostName: "MyHost", PWD: tc.Cwd, Folder: "vagrant", }, Segments: maps.NewConcurrent[any](), } template.Init(env, nil, nil) engine := &Engine{ Config: &config.Config{ ConsoleTitleTemplate: tc.Template, }, Env: env, } title := engine.getTitleTemplateText() got := terminal.FormatTitle(title) assert.Equal(t, tc.Expected, got) } } func TestGetConsoleTitleIfGethostnameReturnsError(t *testing.T) { cases := []struct { Template string User string Cwd string PathSeparator string ShellName string Expected string Root bool }{ { Template: "Not using Host only {{.UserName}} and {{.Shell}}", User: "MyUser", PathSeparator: "\\", ShellName: "PowerShell", Expected: "\x1b]0;Not using Host only MyUser and PowerShell\a", }, { Template: "{{.UserName}}@{{.HostName}} :: {{.Shell}}", User: "MyUser", PathSeparator: "\\", ShellName: "PowerShell", Expected: "\x1b]0;MyUser@ :: PowerShell\a", }, { Template: "\x1b[93m[\x1b[39m\x1b[96mconsole-title\x1b[39m\x1b[96m ≡\x1b[39m\x1b[31m +0\x1b[39m\x1b[31m ~1\x1b[39m\x1b[31m -0\x1b[39m\x1b[31m !\x1b[39m\x1b[93m]\x1b[39m", Expected: "\x1b]0;[console-title ≡ +0 ~1 -0 !]\a", }, } for _, tc := range cases { env := new(mock.Environment) env.On("Pwd").Return(tc.Cwd) env.On("Home").Return("/usr/home") env.On("Getenv", "USERDOMAIN").Return("MyCompany") env.On("Shell").Return(tc.ShellName) terminal.Init(shell.GENERIC) template.Cache = &cache.Template{ SimpleTemplate: cache.SimpleTemplate{ Shell: tc.ShellName, UserName: "MyUser", Root: tc.Root, HostName: "", }, Segments: maps.NewConcurrent[any](), } template.Init(env, nil, nil) engine := &Engine{ Config: &config.Config{ ConsoleTitleTemplate: tc.Template, }, Env: env, } title := engine.getTitleTemplateText() got := terminal.FormatTitle(title) assert.Equal(t, tc.Expected, got) } } func TestShouldFill(t *testing.T) { cases := []struct { Case string Overflow config.Overflow ExpectedFiller string Block config.Block Padding int ExpectedBool bool }{ { Case: "Plain single character with no padding", Padding: 0, ExpectedFiller: "", ExpectedBool: true, Block: config.Block{ Overflow: config.Hide, Filler: "-", }, }, { Case: "Plain single character with 1 padding", Padding: 1, ExpectedFiller: "-", ExpectedBool: true, Block: config.Block{ Overflow: config.Hide, Filler: "-", }, }, { Case: "Plain single character with lots of padding", Padding: 200, ExpectedFiller: strings.Repeat("-", 200), ExpectedBool: true, Block: config.Block{ Overflow: config.Hide, Filler: "-", }, }, { Case: "Plain multi-character with some padding", Padding: 20, ExpectedFiller: strings.Repeat("-^-", 6) + " ", ExpectedBool: true, Block: config.Block{ Overflow: config.Hide, Filler: "-^-", }, }, { Case: "Template conditional on overflow with no overflow", Padding: 3, ExpectedFiller: strings.Repeat("X", 3), ExpectedBool: true, Block: config.Block{ Overflow: config.Hide, Filler: "{{ if .Overflow -}} O {{- else -}} X {{- end }}", }, }, { Case: "Template conditional on overflow with an overflow", Overflow: config.Break, Padding: 3, ExpectedFiller: strings.Repeat("O", 3), ExpectedBool: true, Block: config.Block{ Overflow: config.Hide, Filler: "{{ if .Overflow -}} O {{- else -}} X {{- end }}", }, }, { Case: "Template conditional on overflow break", Overflow: config.Break, Padding: 3, ExpectedFiller: strings.Repeat("O", 3), ExpectedBool: true, Block: config.Block{ Overflow: config.Break, Filler: `{{ if eq .Overflow "break" -}} O {{- else -}} X {{- end }}`, }, }, } for _, tc := range cases { env := new(mock.Environment) env.On("Shell").Return(shell.GENERIC) engine := &Engine{ Env: env, Overflow: tc.Overflow, } template.Cache = &cache.Template{ SimpleTemplate: cache.SimpleTemplate{ Shell: shell.GENERIC, }, Segments: maps.NewConcurrent[any](), } template.Init(env, nil, nil) terminal.Init(shell.GENERIC) terminal.Plain = true terminal.Colors = &color.Defaults{} gotFiller, gotBool := engine.shouldFill(tc.Block.Filler, tc.Padding) assert.Equal(t, tc.ExpectedFiller, gotFiller, tc.Case) assert.Equal(t, tc.ExpectedBool, gotBool, tc.Case) } } ================================================ FILE: src/prompt/extra.go ================================================ package prompt import ( "fmt" "github.com/jandedobbeleer/oh-my-posh/src/color" "github.com/jandedobbeleer/oh-my-posh/src/config" "github.com/jandedobbeleer/oh-my-posh/src/shell" "github.com/jandedobbeleer/oh-my-posh/src/template" "github.com/jandedobbeleer/oh-my-posh/src/terminal" ) type ExtraPromptType int const ( Transient ExtraPromptType = iota Valid Error Secondary Debug ) func (e *Engine) ExtraPrompt(promptType ExtraPromptType) string { var prompt *config.Segment switch promptType { case Debug: prompt = e.Config.DebugPrompt case Transient: prompt = e.Config.TransientPrompt case Valid: prompt = e.Config.ValidLine case Error: prompt = e.Config.ErrorLine case Secondary: prompt = e.Config.SecondaryPrompt } if prompt == nil { prompt = &config.Segment{} } getTemplate := func(template string) string { if len(template) != 0 { return template } switch promptType { //nolint: exhaustive case Debug: return "[DBG]: " case Transient: return "{{ .Shell }}> " case Secondary: return "> " default: return "" } } promptText, err := template.Render(getTemplate(prompt.Template), nil) if err != nil { promptText = err.Error() } if promptType == Transient && prompt.Newline { promptText = fmt.Sprintf("%s%s", e.getNewline(), promptText) } if promptType == Transient && e.Config.ShellIntegration { exitCode, _ := e.Env.StatusCodes() e.write(terminal.CommandFinished(exitCode, e.Env.Flags().NoExitCode)) e.write(terminal.PromptStart()) } foreground := color.Ansi(prompt.ForegroundTemplates.FirstMatch(nil, string(prompt.Foreground))) background := color.Ansi(prompt.BackgroundTemplates.FirstMatch(nil, string(prompt.Background))) terminal.SetColors(background, foreground) terminal.Write(background, foreground, promptText) str, length := terminal.String() if promptType == Transient && len(prompt.Filler) != 0 { consoleWidth, err := e.Env.TerminalWidth() if err == nil || consoleWidth != 0 { if padText, OK := e.shouldFill(prompt.Filler, consoleWidth-length); OK { str += padText } } } switch e.Env.Shell() { case shell.ZSH: if promptType == Transient { if !e.Env.Flags().Eval { break } prompt := fmt.Sprintf("PS1=%s", shell.QuotePosixStr(str)) // empty RPROMPT prompt += "\nRPROMPT=''" return prompt } case shell.PWSH: if promptType == Transient { // clear the line afterwards to prevent text from being written on the same line // see https://github.com/JanDeDobbeleer/oh-my-posh/issues/3628 return str + terminal.ClearAfter() } } return str } ================================================ FILE: src/prompt/preview.go ================================================ package prompt import ( "fmt" "github.com/jandedobbeleer/oh-my-posh/src/log" "github.com/jandedobbeleer/oh-my-posh/src/text" ) func (e *Engine) Preview() string { builder := text.NewBuilder() printPrompt := func(title, prompt string) { builder.WriteString(log.Text(fmt.Sprintf("\n%s:\n\n", title)).Bold().Plain().String()) builder.WriteString(prompt) builder.WriteString("\n") } printPrompt("Primary", e.Primary()) right := e.RPrompt() if len(right) > 0 { printPrompt("Right", right) } if e.Config.SecondaryPrompt != nil { printPrompt("Secondary", e.ExtraPrompt(Secondary)) } if e.Config.TransientPrompt != nil { printPrompt("Transient", e.ExtraPrompt(Transient)) } if e.Config.DebugPrompt != nil { printPrompt("Debug", e.ExtraPrompt(Debug)) } if e.Config.ValidLine != nil { printPrompt("Valid", e.ExtraPrompt(Valid)) } if e.Config.ErrorLine != nil { printPrompt("Error", e.ExtraPrompt(Error)) } builder.WriteString("\n") return builder.String() } ================================================ FILE: src/prompt/primary.go ================================================ package prompt import ( "fmt" "strings" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/config" "github.com/jandedobbeleer/oh-my-posh/src/shell" "github.com/jandedobbeleer/oh-my-posh/src/terminal" ) func (e *Engine) Primary() string { return e.primaryInternal(false) } // primaryInternal handles both regular and streaming prompt rendering func (e *Engine) primaryInternal(fromCache bool) string { needsPrimaryRightPrompt := e.needsPrimaryRightPrompt() e.writePrimaryPromptInternal(needsPrimaryRightPrompt, fromCache) switch e.Env.Shell() { case shell.ZSH: if !e.Env.Flags().Eval { break } // Warp doesn't support RPROMPT so we need to write it manually if e.isWarp() { e.writePrimaryRightPrompt() prompt := fmt.Sprintf("PS1=%s", shell.QuotePosixStr(e.string())) return prompt } prompt := fmt.Sprintf("PS1=%s", shell.QuotePosixStr(e.string())) prompt += fmt.Sprintf("\nRPROMPT=%s", shell.QuotePosixStr(e.rprompt)) return prompt default: if !needsPrimaryRightPrompt { break } e.writePrimaryRightPrompt() } return e.string() } func (e *Engine) writePrimaryPrompt(needsPrimaryRPrompt bool) { e.writePrimaryPromptInternal(needsPrimaryRPrompt, false) } // writePrimaryPromptInternal handles both regular and streaming prompt rendering func (e *Engine) writePrimaryPromptInternal(needsPrimaryRPrompt, fromCache bool) { if e.Config.ShellIntegration { exitCode, _ := e.Env.StatusCodes() e.write(terminal.CommandFinished(exitCode, e.Env.Flags().NoExitCode)) e.write(terminal.PromptStart()) } // cache a pointer to the color cycle cycle = &e.Config.Cycle var cancelNewline, didRender bool // Choose block source based on whether we're rendering from cache blocks := e.Config.Blocks if fromCache { blocks = e.allBlocks } for i, block := range blocks { // do not print a leading newline when we're at the first row and the prompt is cleared if i == 0 { row, _ := e.Env.CursorPosition() cancelNewline = e.Env.Flags().Cleared || e.Env.Flags().PromptCount == 1 || row == 1 } // skip setting a newline when we didn't print anything yet if i != 0 { cancelNewline = !didRender } if block.Type == config.RPrompt && !needsPrimaryRPrompt { continue } // Choose render method based on whether we're rendering from cache var rendered bool if fromCache { rendered = e.renderBlockFromCache(block, cancelNewline) } else { rendered = e.renderBlock(block, cancelNewline) } if rendered { didRender = true } // Only handle tooltip caching in regular (non-cached) rendering if !fromCache && !e.Config.ToolTipsAction.IsDefault() { cache.Set(cache.Session, RPromptKey, e.rprompt, cache.INFINITE) cache.Set(cache.Session, RPromptLengthKey, e.rpromptLength, cache.INFINITE) } } if len(e.Config.ConsoleTitleTemplate) > 0 && !e.Env.Flags().Plain { title := e.getTitleTemplateText() e.write(terminal.FormatTitle(title)) } if e.Config.FinalSpace { e.write(" ") e.currentLineLength++ } if e.Config.ITermFeatures != nil && e.isIterm() { host, _ := e.Env.Host() e.write(terminal.RenderItermFeatures(e.Config.ITermFeatures, e.Env.Shell(), e.Env.Pwd(), e.Env.User(), host)) } if e.Config.ShellIntegration { e.write(terminal.CommandStart()) } e.pwd() } func (e *Engine) needsPrimaryRightPrompt() bool { if e.Env.Flags().Debug { return true } switch e.Env.Shell() { case shell.PWSH, shell.GENERIC, shell.ZSH: return true default: return false } } func (e *Engine) writePrimaryRightPrompt() { space, OK := e.canWriteRightBlock(e.rpromptLength, true) if !OK { return } e.write(terminal.SaveCursorPosition()) e.write(strings.Repeat(" ", space)) e.write(e.rprompt) e.write(terminal.RestoreCursorPosition()) } ================================================ FILE: src/prompt/rprompt.go ================================================ package prompt import ( "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/config" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/shell" ) const ( RPromptKey = "rprompt" RPromptLengthKey = "rprompt_length" ) func (e *Engine) RPrompt() string { var rprompt *config.Block for _, block := range e.Config.Blocks { if block.Type != config.RPrompt { continue } rprompt = block break } if rprompt == nil { return "" } text, length := e.writeBlockSegments(rprompt) // do not print anything when we don't have any text if length == 0 { return "" } e.rpromptLength = length if e.Env.Shell() == shell.ELVISH && e.Env.GOOS() != runtime.WINDOWS { // Workaround to align with a right-aligned block on non-Windows systems. text += " " } if !e.Config.ToolTipsAction.IsDefault() { cache.Set(cache.Session, RPromptKey, text, cache.INFINITE) cache.Set(cache.Session, RPromptLengthKey, e.rpromptLength, cache.INFINITE) } return text } ================================================ FILE: src/prompt/segments.go ================================================ package prompt import ( "time" "github.com/jandedobbeleer/oh-my-posh/src/config" "github.com/jandedobbeleer/oh-my-posh/src/log" runjobs "github.com/jandedobbeleer/oh-my-posh/src/runtime/jobs" "github.com/jandedobbeleer/oh-my-posh/src/terminal" ) type result struct { segment *config.Segment index int } func (e *Engine) writeBlockSegments(block *config.Block) (string, int) { length := len(block.Segments) if length == 0 { return "", 0 } out := make(chan result, length) e.writeSegmentsConcurrently(block.Segments, out) e.writeSegments(out, block) if e.activeSegment != nil && len(block.TrailingDiamond) > 0 { e.activeSegment.TrailingDiamond = block.TrailingDiamond } e.writeSeparator(true) e.activeSegment = nil e.previousActiveSegment = nil return terminal.String() } // writeSegmentsConcurrently uses individual goroutines for each segment func (e *Engine) writeSegmentsConcurrently(segments []*config.Segment, out chan result) { for i, segment := range segments { // In streaming mode, pre-register all segments as pending // This ensures countPendingSegments() sees them before timeout occurs if e.Env.Flags().Streaming { segment.Timeout = e.Config.Streaming e.pendingSegments.Store(segment.Name(), true) } go func(segment *config.Segment, index int) { if segment.Timeout > 0 { e.executeSegmentWithTimeout(segment) } else { segment.Execute(e.Env) } out <- result{segment, index} // In streaming mode, clean up pre-registered segments that completed before timeout if e.Env.Flags().Streaming && segment.Timeout > 0 && !segment.Pending { e.pendingSegments.Delete(segment.Name()) } }(segment, i) } } // executeSegmentWithTimeout handles segment execution with timeout logic func (e *Engine) executeSegmentWithTimeout(segment *config.Segment) { done := make(chan bool) gidChan := make(chan uint64, 1) go func() { gidChan <- runjobs.CurrentGID() segment.Execute(e.Env) close(done) }() gid := <-gidChan select { case <-done: // Completed before timeout - nothing extra to do case <-time.After(time.Duration(segment.Timeout) * time.Millisecond): log.Errorf("timeout after %dms for segment: %s", segment.Timeout, segment.Name()) // When streaming is enabled, don't kill goroutines - let them continue executing if e.Env.Flags().Streaming { segment.Pending = true // Note: Do NOT set segment.Enabled here - that would race with Execute() // Rendering logic handles Pending state to display "..." text // Track this segment as pending and continue execution in background e.trackPendingSegment(segment, done) return } // For non-streaming mode, kill the goroutine if err := runjobs.KillGoroutineChildren(gid); err != nil { log.Errorf("failed to kill child processes for goroutine %d (segment: %s): %v", gid, segment.Name(), err) } } } func (e *Engine) writeSegments(out chan result, block *config.Block) { count := len(block.Segments) current := 0 executedCount := 0 results := make([]*config.Segment, count) // Pre-allocate map with known capacity to reduce allocations executed := make(map[string]bool, count) segmentIndex := 0 // Process results as they come in, eliminating busy waiting for executedCount < count { res := <-out // Block until result is available executedCount++ results[res.index] = res.segment executed[res.segment.Name()] = true // Process segments that can now be rendered for current < count && results[current] != nil { segment := results[current] if !e.canRenderSegment(segment, executed) { break } if segment.Render(segmentIndex, e.forceRender) { segmentIndex++ } e.writeSegment(block, segment) current++ } } // render all remaining segments where the needs can't be resolved for current < executedCount { segment := results[current] if segment.Render(segmentIndex, e.forceRender) { segmentIndex++ } e.writeSegment(block, segment) current++ } } func (e *Engine) writeSegment(block *config.Block, segment *config.Segment) { // Allow pending segments to render (they show "..." text) if !segment.Pending && !segment.Enabled && segment.ResolveStyle() != config.Accordion { return } if colors, newCycle := cycle.Loop(); colors != nil { cycle = &newCycle segment.Foreground = colors.Foreground segment.Background = colors.Background } if terminal.Len() == 0 && len(block.LeadingDiamond) > 0 { segment.LeadingDiamond = block.LeadingDiamond } e.setActiveSegment(segment) e.renderActiveSegment() } // canRenderSegment now uses map for O(1) lookups instead of O(n) slice search func (e *Engine) canRenderSegment(segment *config.Segment, executed map[string]bool) bool { for _, name := range segment.Needs { if !executed[name] { return false } } return true } ================================================ FILE: src/prompt/segments_test.go ================================================ package prompt import ( "testing" "time" "github.com/jandedobbeleer/oh-my-posh/src/config" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/runtime/mock" "github.com/stretchr/testify/assert" ) func TestRenderBlock(t *testing.T) { engine := New(&runtime.Flags{ IsPrimary: true, }) block := &config.Block{ Segments: []*config.Segment{ { Type: "text", Template: "Hello", Foreground: "red", Background: "blue", }, { Type: "text", Template: "World", Foreground: "red", Background: "blue", }, }, } prompt, length := engine.writeBlockSegments(block) assert.Equal(t, "\x1b[44m\x1b[31mHello\x1b[0m\x1b[44m\x1b[31mWorld\x1b[0m", prompt) assert.Equal(t, 10, length) } func TestCanRenderSegment(t *testing.T) { cases := []struct { Case string Executed map[string]bool Needs []string Expected bool }{ { Case: "No cross segment dependencies", Expected: true, }, { Case: "Cross segment dependencies, nothing executed", Expected: false, Needs: []string{"Foo"}, }, { Case: "Cross segment dependencies, available", Expected: true, Executed: map[string]bool{ "Foo": true, }, Needs: []string{"Foo"}, }, } for _, c := range cases { segment := &config.Segment{ Type: "text", Needs: c.Needs, } engine := &Engine{} got := engine.canRenderSegment(segment, c.Executed) assert.Equal(t, c.Expected, got, c.Case) } } func TestExecuteSegmentWithTimeout_Streaming(t *testing.T) { // This test verifies that when streaming is enabled and a timeout occurs, // the segment is marked as pending and tracked for later completion env := new(mock.Environment) env.On("Flags").Return(&runtime.Flags{Streaming: true}) segment := &config.Segment{ Type: "text", Timeout: 1, // Very short timeout to ensure it triggers } engine := &Engine{ Env: env, streamingResults: make(chan *config.Segment, 10), } // Create a mock segment that will definitely timeout // We'll use the actual timeout mechanism by making the execution slow done := make(chan bool) go func() { time.Sleep(100 * time.Millisecond) // Longer than timeout close(done) }() // Pre-register segment as pending (this happens in writeSegmentsConcurrently) engine.pendingSegments.Store(segment.Name(), true) // Mark as pending and track (simulating what executeSegmentWithTimeout does) segment.Pending = true engine.trackPendingSegment(segment, done) // Verify it was tracked as pending _, exists := engine.pendingSegments.Load(segment.Name()) assert.True(t, exists, "Segment should be tracked as pending") // Wait for completion notification select { case completed := <-engine.streamingResults: assert.Equal(t, segment, completed) assert.False(t, completed.Pending, "Segment should no longer be pending after completion") case <-time.After(200 * time.Millisecond): t.Error("Expected segment completion notification") } } func TestExecuteSegmentWithTimeout_NonStreaming(t *testing.T) { // This test verifies that when streaming is disabled, // trackPendingSegment returns early without tracking when streamingResults is nil segment := &config.Segment{ Type: "text", Timeout: 10, } engine := &Engine{ // streamingResults is nil (non-streaming mode) } done := make(chan bool) // Pre-register segment (simulating what happens in concurrent execution) engine.pendingSegments.Store(segment.Name(), true) // trackPendingSegment should not track when streamingResults is nil engine.trackPendingSegment(segment, done) // Signal completion close(done) // Give time for any goroutine to run (shouldn't be one) time.Sleep(50 * time.Millisecond) // Segment should still be in pendingSegments because notifySegmentCompletion // was never called (trackPendingSegment returns early when streamingResults is nil) _, exists := engine.pendingSegments.Load(segment.Name()) assert.True(t, exists, "Segment should remain in pendingSegments when streaming is disabled") } func TestExecuteSegmentWithTimeout_CachedValueFallback(t *testing.T) { // This test verifies that a pending segment's Text() returns "..." placeholder env := new(mock.Environment) env.On("Flags").Return(&runtime.Flags{}) segment := &config.Segment{ Type: "text", Pending: true, Template: "actual content", } // Initialize the segment writer err := segment.MapSegmentWithWriter(env) assert.NoError(t, err) // Render with pending state - should show "..." segment.Render(0, true) text := segment.Text() assert.Equal(t, "...", text, "Pending segment should show ...") // After completion, render again with actual content segment.Pending = false segment.Render(0, true) text = segment.Text() assert.NotEqual(t, "...", text, "Non-pending segment should show actual content") } ================================================ FILE: src/prompt/status.go ================================================ package prompt func (e *Engine) Status() string { e.writePrimaryPrompt(false) return e.string() } ================================================ FILE: src/prompt/streaming.go ================================================ package prompt import ( "github.com/jandedobbeleer/oh-my-posh/src/config" ) // StreamPrimary returns a channel that yields prompt updates as segments complete. func (e *Engine) StreamPrimary() <-chan string { // Initialize streaming infrastructure BEFORE launching goroutine // This ensures the channel exists when segments start timing out e.streamingResults = make(chan *config.Segment, 100) e.allBlocks = e.Config.Blocks out := make(chan string, 10) go func() { defer close(out) defer close(e.streamingResults) // Render and send initial prompt with pending segments initialPrompt := e.Primary() out <- initialPrompt if e.countPendingSegments() == 0 { return } // Listen for segment completions for range e.streamingResults { out <- e.renderFromBlocks() if e.countPendingSegments() == 0 { return } } }() return out } // countPendingSegments counts how many segments are marked as pending func (e *Engine) countPendingSegments() int { count := 0 e.pendingSegments.Range(func(_, _ any) bool { count++ return true }) return count } // renderFromBlocks re-renders the complete prompt using stored block data func (e *Engine) renderFromBlocks() string { // Reset prompt builder e.prompt.Reset() e.currentLineLength = 0 e.activeSegment = nil e.previousActiveSegment = nil e.rprompt = "" e.rpromptLength = 0 return e.primaryInternal(true) } // trackPendingSegment continues execution for a timed-out segment in the background func (e *Engine) trackPendingSegment(segment *config.Segment, done chan bool) { if e.streamingResults == nil { return } // Segment is already pre-registered in pendingSegments map go func() { <-done segment.Pending = false e.notifySegmentCompletion(segment) }() } // notifySegmentCompletion sends completed segment to the streaming results channel func (e *Engine) notifySegmentCompletion(segment *config.Segment) { if e.streamingResults == nil { return } if _, ok := e.pendingSegments.LoadAndDelete(segment.Name()); ok { select { case e.streamingResults <- segment: // Successfully notified consumer default: // Consumer not ready or already exited // This can happen if segment completes after consumer finishes } } } ================================================ FILE: src/prompt/streaming_test.go ================================================ package prompt import ( "testing" "time" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/color" "github.com/jandedobbeleer/oh-my-posh/src/config" "github.com/jandedobbeleer/oh-my-posh/src/maps" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/runtime/mock" "github.com/jandedobbeleer/oh-my-posh/src/shell" "github.com/jandedobbeleer/oh-my-posh/src/template" "github.com/jandedobbeleer/oh-my-posh/src/terminal" "github.com/stretchr/testify/assert" testifymock "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) func TestStreamPrimary_NoSegments(t *testing.T) { env := new(mock.Environment) env.On("Pwd").Return("/test") env.On("Home").Return("/home") env.On("Shell").Return(shell.PWSH) env.On("Flags").Return(&runtime.Flags{Streaming: true}) env.On("CursorPosition").Return(1, 1) env.On("StatusCodes").Return(0, "0") template.Cache = &cache.Template{ Segments: maps.NewConcurrent[any](), } template.Init(env, nil, nil) terminal.Init(shell.PWSH) engine := &Engine{ Config: &config.Config{ Blocks: []*config.Block{}, }, Env: env, } out := engine.StreamPrimary() prompts := collectChannelOutput(out, 100*time.Millisecond) // Should get exactly one prompt (initial) with no pending segments assert.Len(t, prompts, 1) } func TestStreamPrimary_WithPendingSegments(t *testing.T) { engine := &Engine{ streamingResults: make(chan *config.Segment, 10), } segment := &config.Segment{ Type: "text", Pending: true, } // Track as pending engine.pendingSegments.Store(segment.Name(), true) // Simulate segment completion in background go func() { time.Sleep(50 * time.Millisecond) segment.Pending = false engine.notifySegmentCompletion(segment) }() // Verify notification is received select { case completed := <-engine.streamingResults: assert.Equal(t, segment, completed) assert.False(t, completed.Pending) case <-time.After(200 * time.Millisecond): t.Error("Expected segment completion notification") } } func TestCountPendingSegments(t *testing.T) { cases := []struct { Case string Segments []string Count int }{ {Case: "No pending segments", Count: 0, Segments: []string{}}, {Case: "One pending segment", Count: 1, Segments: []string{"segment1"}}, {Case: "Multiple pending segments", Count: 3, Segments: []string{"segment1", "segment2", "segment3"}}, } for _, tc := range cases { engine := &Engine{} for _, name := range tc.Segments { engine.pendingSegments.Store(name, true) } count := engine.countPendingSegments() assert.Equal(t, tc.Count, count, tc.Case) } } func TestNotifySegmentCompletion(t *testing.T) { cases := []struct { Case string StreamingSetup bool SegmentPending bool ExpectNotify bool }{ {Case: "No streaming channel", StreamingSetup: false, SegmentPending: true, ExpectNotify: false}, {Case: "Segment not pending", StreamingSetup: true, SegmentPending: false, ExpectNotify: false}, {Case: "Valid notification", StreamingSetup: true, SegmentPending: true, ExpectNotify: true}, } for _, tc := range cases { engine := &Engine{} segment := &config.Segment{Type: "test"} if tc.StreamingSetup { engine.streamingResults = make(chan *config.Segment, 10) } if tc.SegmentPending { engine.pendingSegments.Store(segment.Name(), true) } engine.notifySegmentCompletion(segment) if tc.ExpectNotify { select { case received := <-engine.streamingResults: assert.Equal(t, segment, received, tc.Case) case <-time.After(100 * time.Millisecond): t.Errorf("%s: Expected notification but got timeout", tc.Case) } } else if tc.StreamingSetup { select { case <-engine.streamingResults: t.Errorf("%s: Unexpected notification received", tc.Case) case <-time.After(50 * time.Millisecond): // Expected - no notification } } } } func TestTrackPendingSegment(t *testing.T) { engine := &Engine{ streamingResults: make(chan *config.Segment, 10), } segment := &config.Segment{ Type: "test", Pending: true, } done := make(chan bool) // Pre-register segment as pending (this happens in writeSegmentsConcurrently in real code) engine.pendingSegments.Store(segment.Name(), true) // Start tracking engine.trackPendingSegment(segment, done) // Verify segment is tracked _, ok := engine.pendingSegments.Load(segment.Name()) assert.True(t, ok, "Segment should be tracked") // Simulate completion close(done) // Wait for goroutine to process select { case completed := <-engine.streamingResults: assert.Equal(t, segment, completed) assert.False(t, segment.Pending, "Segment should no longer be pending") case <-time.After(100 * time.Millisecond): t.Error("Expected segment completion notification") } // Verify segment is no longer tracked _, ok = engine.pendingSegments.Load(segment.Name()) assert.False(t, ok, "Segment should no longer be tracked") } func TestRenderFromBlocks(_ *testing.T) { env := new(mock.Environment) env.On("Shell").Return(shell.PWSH) env.On("Flags").Return(&runtime.Flags{}) // This test validates that renderFromBlocks properly delegates to primaryInternal engine := &Engine{ Config: &config.Config{ Blocks: []*config.Block{}, }, Env: env, allBlocks: []*config.Block{}, } // Just verify it doesn't panic - full integration tested elsewhere _ = engine.renderFromBlocks() } func TestPrimaryInternal_FromCache(_ *testing.T) { env := new(mock.Environment) env.On("Shell").Return(shell.PWSH) env.On("Flags").Return(&runtime.Flags{}) // This test validates the fromCache parameter is handled correctly engine := &Engine{ Config: &config.Config{ Blocks: []*config.Block{}, }, Env: env, allBlocks: []*config.Block{}, } // Just verify it doesn't panic - full integration tested elsewhere _ = engine.primaryInternal(true) } func TestRenderBlockFromCache(t *testing.T) { // This test validates renderBlockFromCache handles segments correctly segment := &config.Segment{ Type: "text", Enabled: false, } block := &config.Block{ Type: config.Prompt, Alignment: config.Left, Segments: []*config.Segment{segment}, } engine := &Engine{ Config: &config.Config{}, } terminal.Init(shell.PWSH) // Should not render when segment is disabled and not forced result := engine.renderBlockFromCache(block, false) assert.False(t, result, "Block should not render with disabled segment") } func TestSegmentPendingState(t *testing.T) { env := new(mock.Environment) env.On("Shell").Return(shell.PWSH) env.On("Flags").Return(&runtime.Flags{}) template.Cache = &cache.Template{ Segments: maps.NewConcurrent[any](), } template.Init(env, nil, nil) segment := &config.Segment{ Type: "text", Pending: true, Template: "test template", } err := segment.MapSegmentWithWriter(env) require.NoError(t, err) // Render with pending state - should show "..." segment.Render(0, true) text := segment.Text() assert.Equal(t, "...", text, "Pending segment should show ...") // After completion segment.Pending = false segment.Render(0, true) text = segment.Text() assert.NotEqual(t, "...", text, "Non-pending segment should show actual content") } // Helper function to collect all output from a channel with timeout func collectChannelOutput(ch <-chan string, timeout time.Duration) []string { var results []string timer := time.NewTimer(timeout) defer timer.Stop() for { select { case result, ok := <-ch: if !ok { return results } results = append(results, result) case <-timer.C: return results } } } func TestStreamingWithTimeout(t *testing.T) { engine := &Engine{ streamingResults: make(chan *config.Segment, 10), } segment := &config.Segment{ Type: "test", Timeout: 10, } // Pre-register segment as pending (this happens in writeSegmentsConcurrently in real code) engine.pendingSegments.Store(segment.Name(), true) // Test that timeout with streaming enabled marks segment as pending done := make(chan bool) go func() { time.Sleep(50 * time.Millisecond) close(done) }() engine.trackPendingSegment(segment, done) // Verify pending state _, isPending := engine.pendingSegments.Load(segment.Name()) require.True(t, isPending, "Segment should be pending") // Wait for completion select { case <-engine.streamingResults: // Success case <-time.After(200 * time.Millisecond): t.Error("Timeout waiting for segment completion") } // Verify no longer pending _, isPending = engine.pendingSegments.Load(segment.Name()) assert.False(t, isPending, "Segment should no longer be pending") } func setupStreamingTestEnv() *mock.Environment { env := new(mock.Environment) env.On("Pwd").Return("/test") env.On("Home").Return("/home") env.On("Shell").Return(shell.PWSH) env.On("Flags").Return(&runtime.Flags{Streaming: true}) env.On("CursorPosition").Return(1, 1) env.On("StatusCodes").Return(0, "0") env.On("DirMatchesOneOf", testifymock.Anything, testifymock.Anything).Return(false) // Mock accent color retrieval for both Windows and macOS env.On("RunCommand", testifymock.Anything, testifymock.Anything, testifymock.Anything, testifymock.Anything).Return("4", nil) env.On("WindowsRegistryKeyValue", testifymock.Anything).Return(&runtime.WindowsRegistryValue{ValueType: runtime.DWORD, DWord: 0xFF0078D7}, nil) template.Cache = &cache.Template{ Segments: maps.NewConcurrent[any](), } template.Init(env, nil, nil) terminal.Init(shell.PWSH) terminal.Colors = color.MakeColors(nil, false, "", env) return env } func TestStreamPrimary_FullFlow_WithRendering(t *testing.T) { env := setupStreamingTestEnv() // Create segments with different speeds fastSegment := &config.Segment{ Type: "text", Template: "FAST", Foreground: "#ffffff", Background: "#000000", } slowSegment := &config.Segment{ Type: "text", Template: "SLOW", Pending: true, // Initially pending Foreground: "#ffffff", Background: "#000000", } engine := &Engine{ Config: &config.Config{ Blocks: []*config.Block{ { Type: config.Prompt, Alignment: config.Left, Segments: []*config.Segment{fastSegment, slowSegment}, }, }, }, Env: env, streamingResults: make(chan *config.Segment, 10), } // Map segment writers err := fastSegment.MapSegmentWithWriter(env) require.NoError(t, err) err = slowSegment.MapSegmentWithWriter(env) require.NoError(t, err) // Track slow segment as pending engine.pendingSegments.Store(slowSegment.Name(), true) // Start streaming out := engine.StreamPrimary() // Simulate slow segment completion after delay go func() { time.Sleep(50 * time.Millisecond) slowSegment.Pending = false engine.notifySegmentCompletion(slowSegment) }() // Collect all prompts prompts := collectChannelOutput(out, 200*time.Millisecond) // Should have at least 2 prompts: initial (with "...") and final (with "SLOW") assert.GreaterOrEqual(t, len(prompts), 1, "Should have at least initial prompt") // First prompt should contain "..." for pending segment if len(prompts) > 0 { assert.Contains(t, prompts[0], "...", "Initial prompt should show pending text") } // If we got multiple prompts, last one should not have "..." if len(prompts) > 1 { assert.NotContains(t, prompts[len(prompts)-1], "...", "Final prompt should not show pending text") } } func TestStreamPrimary_MultipleBlocks_MixedSpeed(t *testing.T) { env := setupStreamingTestEnv() // Block 1: Fast segment fast1 := &config.Segment{ Type: "text", Template: "FAST1", } // Block 2: Slow segment slow1 := &config.Segment{ Type: "text", Template: "SLOW1", Pending: true, } // Block 3: Another fast segment fast2 := &config.Segment{ Type: "text", Template: "FAST2", } engine := &Engine{ Config: &config.Config{ Blocks: []*config.Block{ {Type: config.Prompt, Alignment: config.Left, Segments: []*config.Segment{fast1}}, {Type: config.Prompt, Alignment: config.Left, Segments: []*config.Segment{slow1}}, {Type: config.Prompt, Alignment: config.Left, Segments: []*config.Segment{fast2}}, }, }, Env: env, streamingResults: make(chan *config.Segment, 10), } // Map segments require.NoError(t, fast1.MapSegmentWithWriter(env)) require.NoError(t, slow1.MapSegmentWithWriter(env)) require.NoError(t, fast2.MapSegmentWithWriter(env)) // Track slow segment engine.pendingSegments.Store(slow1.Name(), true) // Start streaming out := engine.StreamPrimary() // Simulate completion go func() { time.Sleep(50 * time.Millisecond) slow1.Pending = false engine.notifySegmentCompletion(slow1) }() prompts := collectChannelOutput(out, 200*time.Millisecond) // Should receive prompts assert.NotEmpty(t, prompts, "Should receive streaming prompts") } func setupBasicStreamingTestEnv() *Engine { env := new(mock.Environment) env.On("Pwd").Return("/test") env.On("Home").Return("/home") env.On("Shell").Return(shell.PWSH) env.On("Flags").Return(&runtime.Flags{Streaming: true}) env.On("CursorPosition").Return(1, 1) env.On("StatusCodes").Return(0, "0") template.Cache = &cache.Template{ Segments: maps.NewConcurrent[any](), } template.Init(env, nil, nil) terminal.Init(shell.PWSH) engine := &Engine{ Config: &config.Config{ Blocks: []*config.Block{}, }, Env: env, } return engine } func TestStreamPrimary_EarlyChannelClosure(t *testing.T) { engine := setupBasicStreamingTestEnv() // Start streaming with no pending segments // The goroutine should complete quickly and close channels properly out := engine.StreamPrimary() // Should be able to read from output channel without panic prompts := collectChannelOutput(out, 100*time.Millisecond) // Should get exactly one prompt (initial) with no pending segments assert.Len(t, prompts, 1, "Should receive initial prompt") } func TestStreamPrimary_NoStreamingResults_Channel(t *testing.T) { engine := setupBasicStreamingTestEnv() // Engine without streamingResults channel (edge case) // No streamingResults channel set // Should not panic out := engine.StreamPrimary() prompts := collectChannelOutput(out, 100*time.Millisecond) assert.Len(t, prompts, 1, "Should get exactly one prompt with no pending segments") } // TestStreamPrimary_RaceConditionFix validates that the streaming loop // correctly handles segments that complete after Primary() but before/during // the counting phase. This tests the fix for the race where pendingCount // could get out of sync with actual pending segments. func TestStreamPrimary_RaceConditionFix(t *testing.T) { env := new(mock.Environment) env.On("Pwd").Return("/test") env.On("Home").Return("/home") env.On("Shell").Return(shell.PWSH) env.On("Flags").Return(&runtime.Flags{Streaming: true}) env.On("CursorPosition").Return(1, 1) env.On("StatusCodes").Return(0, "0") template.Cache = &cache.Template{ Segments: maps.NewConcurrent[any](), } template.Init(env, nil, nil) terminal.Init(shell.PWSH) engine := &Engine{ Config: &config.Config{ Blocks: []*config.Block{}, }, Env: env, streamingResults: make(chan *config.Segment, 10), } // Create three segments, simulating the race scenario: // - segmentA: Completes quickly after Primary() // - segmentB: Completes during loop // - segmentC: Completes last segmentA := &config.Segment{Type: "test-a", Pending: true} segmentB := &config.Segment{Type: "test-b", Pending: true} segmentC := &config.Segment{Type: "test-c", Pending: true} // Pre-register all three as pending (simulates timeout during Primary()) engine.pendingSegments.Store(segmentA.Name(), true) engine.pendingSegments.Store(segmentB.Name(), true) engine.pendingSegments.Store(segmentC.Name(), true) // Simulate segmentA completing immediately after Primary() but before countPendingSegments() // This is the race condition - notification sent but segment removed from map go func() { // Small delay to ensure StreamPrimary has been called but before counting time.Sleep(5 * time.Millisecond) segmentA.Pending = false engine.notifySegmentCompletion(segmentA) }() // Simulate segmentB and segmentC completing during the loop go func() { time.Sleep(30 * time.Millisecond) segmentB.Pending = false engine.notifySegmentCompletion(segmentB) }() go func() { time.Sleep(50 * time.Millisecond) segmentC.Pending = false engine.notifySegmentCompletion(segmentC) }() // Start streaming out := engine.StreamPrimary() // Collect all prompts with sufficient timeout prompts := collectChannelOutput(out, 200*time.Millisecond) // With the fix, we should receive updates for all three segments // Initial prompt + 3 updates (A, B, C) = 4 total // Without the fix, we might only get Initial + 2 updates and exit early assert.GreaterOrEqual(t, len(prompts), 3, "Should receive updates for all pending segments") // Verify all segments were properly cleaned up count := engine.countPendingSegments() assert.Equal(t, 0, count, "All pending segments should be cleared") } ================================================ FILE: src/prompt/tooltip.go ================================================ package prompt import ( "slices" "strings" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/config" "github.com/jandedobbeleer/oh-my-posh/src/shell" "github.com/jandedobbeleer/oh-my-posh/src/terminal" ) func (e *Engine) Tooltip(tip string) string { tip = strings.Trim(tip, " ") tooltips := make([]*config.Segment, 0, 1) for _, tooltip := range e.Config.Tooltips { if !slices.Contains(tooltip.Tips, tip) { continue } tooltip.Execute(e.Env) if !tooltip.Enabled { continue } tooltips = append(tooltips, tooltip) } if len(tooltips) == 0 { return "" } // little hack to reuse the current logic block := &config.Block{ Alignment: config.Right, Segments: tooltips, } text, length := e.writeBlockSegments(block) // do not print anything when we don't have any text if length == 0 { return "" } text, length = e.handleToolTipAction(text, length) switch e.Env.Shell() { case shell.PWSH: e.rprompt = text e.currentLineLength = e.Env.Flags().Column space, ok := e.canWriteRightBlock(length, true) if !ok { return "" } e.write(terminal.SaveCursorPosition()) e.write(strings.Repeat(" ", space)) e.write(text) e.write(terminal.RestoreCursorPosition()) return e.string() default: return text } } func (e *Engine) handleToolTipAction(text string, length int) (string, int) { if e.Config.ToolTipsAction.IsDefault() { return text, length } rprompt, OK := cache.Get[string](cache.Session, RPromptKey) if !OK { return text, length } rpromptLength, OK := cache.Get[int](cache.Session, RPromptLengthKey) if !OK { return text, length } length += rpromptLength switch e.Config.ToolTipsAction { case config.Extend: text = rprompt + text case config.Prepend: text += rprompt } return text, length } ================================================ FILE: src/regex/regex.go ================================================ package regex import ( "regexp" "sync" "github.com/jandedobbeleer/oh-my-posh/src/log" ) var ( regexCache = make(map[string]*regexp.Regexp) regexCacheLock = sync.RWMutex{} ) const ( LINK = `(?P\x1b]8;;(.+)\x1b\\(?P.+)\x1b]8;;\x1b\\)` ) func GetCompiledRegex(pattern string) (*regexp.Regexp, error) { // try in cache first regexCacheLock.RLock() re := regexCache[pattern] regexCacheLock.RUnlock() if re != nil { return re, nil } // should we panic or return the error? re, err := regexp.Compile(pattern) if err != nil { log.Error(err) return nil, err } // lock for concurrent access and save the compiled expression in cache regexCacheLock.Lock() regexCache[pattern] = re regexCacheLock.Unlock() return re, nil } func FindNamedRegexMatch(pattern, text string) map[string]string { result := make(map[string]string) re, err := GetCompiledRegex(pattern) if err != nil { return result } match := re.FindStringSubmatch(text) if len(match) == 0 { return result } for i, name := range re.SubexpNames() { if i == 0 { continue } result[name] = match[i] } return result } func FindAllNamedRegexMatch(pattern, text string) []map[string]string { var results []map[string]string re, err := GetCompiledRegex(pattern) if err != nil { return results } match := re.FindAllStringSubmatch(text, -1) if len(match) == 0 { return results } for _, set := range match { result := make(map[string]string) for i, name := range re.SubexpNames() { if i == 0 { result["text"] = set[i] continue } result[name] = set[i] } results = append(results, result) } return results } func ReplaceAllString(pattern, text, replaceText string) string { re, err := GetCompiledRegex(pattern) if err != nil { return text } return re.ReplaceAllString(text, replaceText) } func MatchString(pattern, text string) bool { re, err := GetCompiledRegex(pattern) if err != nil { return false } return re.MatchString(text) } func FindStringMatch(pattern, text string, index int) (string, bool) { re, err := GetCompiledRegex(pattern) if err != nil { return text, false } matches := re.FindStringSubmatch(text) if len(matches) <= index { return text, false } match := matches[index] if len(match) == 0 { return text, false } return match, true } ================================================ FILE: src/regex/regex_test.go ================================================ package regex import ( "testing" "github.com/stretchr/testify/assert" ) func TestFindStringMatch(t *testing.T) { cases := []struct { Case string Pattern string Text string Expected string Index int }{ { Case: "Full match at index 0", Pattern: `\w+`, Text: "hello", Index: 0, Expected: "hello", }, { Case: "Capture group at index 1", Pattern: `hello (\w+)`, Text: "hello world", Index: 1, Expected: "world", }, { Case: "No matches returns original text", Pattern: `\d+`, Text: "hello", Index: 0, Expected: "hello", }, { Case: "Invalid pattern returns original text", Pattern: `[invalid`, Text: "hello", Index: 0, Expected: "hello", }, { Case: "Empty text returns empty string", Pattern: `\w+`, Text: "", Index: 0, Expected: "", }, { Case: "Index out of bounds returns original text", Pattern: `(\w+)`, Text: "hello", Index: 2, Expected: "hello", }, { Case: "Multiple capture groups", Pattern: `(\w+)\s(\w+)`, Text: "hello world", Index: 2, Expected: "world", }, } for _, tc := range cases { got, _ := FindStringMatch(tc.Pattern, tc.Text, tc.Index) assert.Equal(t, tc.Expected, got, tc.Case) } } ================================================ FILE: src/runtime/battery/battery.go ================================================ // Copyright (C) 2016-2017 Karol 'Kenji Takahashi' Woźniak // // Permission is hereby granted, free of charge, to any person obtaining // a copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included // in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. // IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, // TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE // OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package battery type Info struct { Percentage int State State } type NoBatteryError struct{} func (m *NoBatteryError) Error() string { return "no battery" } // State type enumerates possible battery states. type State int var states = [...]string{ Unknown: "Unknown", Empty: "Empty", Full: "Full", Charging: "Charging", Discharging: "Discharging", NotCharging: "Not Charging", } func (s State) String() string { return states[s] } // Possible state values. // Unknown can mean either controller returned unknown, or // not able to retrieve state due to some error. const ( Unknown State = iota Empty Full Charging Discharging NotCharging ) ================================================ FILE: src/runtime/battery/battery_darwin.go ================================================ package battery import ( "errors" "strconv" "strings" "github.com/jandedobbeleer/oh-my-posh/src/regex" "github.com/jandedobbeleer/oh-my-posh/src/runtime/cmd" ) func mapMostLogicalState(state string) State { switch state { case "charging": return Charging case "discharging": return Discharging case "AC attached": return NotCharging case "full": return Full case "empty": return Empty case "charged": return Full default: return Unknown } } func parseBatteryOutput(output string) (*Info, error) { matches := regex.FindNamedRegexMatch(`(?P[0-9]{1,3})%; (?P[a-zA-Z\s]+);`, output) if len(matches) != 2 { return nil, errors.New("unable to find battery state based on output") } var percentage int var err error if percentage, err = strconv.Atoi(matches["PERCENTAGE"]); err != nil { return nil, errors.New("unable to parse battery percentage") } // sometimes it reports discharging when at 100, so let's force it to Full // https://github.com/JanDeDobbeleer/oh-my-posh/issues/3729 if percentage == 100 { return &Info{ Percentage: percentage, State: Full, }, nil } return &Info{ Percentage: percentage, State: mapMostLogicalState(matches["STATE"]), }, nil } func Get() (*Info, error) { output, err := cmd.Run("pmset", "-g", "batt") if err != nil { return nil, err } if !strings.Contains(output, "Battery") { return nil, ErrNotFound } return parseBatteryOutput(output) } ================================================ FILE: src/runtime/battery/battery_darwin_test.go ================================================ package battery import ( "testing" "github.com/stretchr/testify/assert" ) func TestParseBatteryOutput(t *testing.T) { cases := []struct { Case string Output string ExpectedState State ExpectedPercentage int ExpectError bool }{ { Case: "charging", Output: "99%; charging;", ExpectedState: Charging, ExpectedPercentage: 99, }, { Case: "charging 1%", Output: "1%; charging;", ExpectedState: Charging, ExpectedPercentage: 1, }, { Case: "not charging 80%", Output: "81%; AC attached;", ExpectedState: NotCharging, ExpectedPercentage: 81, }, { Case: "charged", Output: "100%; charged;", ExpectedState: Full, ExpectedPercentage: 100, }, { Case: "discharging, but not", Output: "100%; discharging;", ExpectedState: Full, ExpectedPercentage: 100, }, } for _, tc := range cases { info, err := parseBatteryOutput(tc.Output) if tc.ExpectError { assert.Error(t, err, tc.Case) return } assert.Equal(t, tc.ExpectedState, info.State, tc.Case) assert.Equal(t, tc.ExpectedPercentage, info.Percentage, tc.Case) } } ================================================ FILE: src/runtime/battery/battery_linux.go ================================================ // battery // Copyright (C) 2016-2017 Karol 'Kenji Takahashi' Woźniak // // Permission is hereby granted, free of charge, to any person obtaining // a copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included // in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. // IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, // TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE // OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package battery import ( "errors" "fmt" "os" "path/filepath" "strconv" "strings" ) const sysfs = "/sys/class/power_supply" func newState(name string) (State, error) { for i, state := range states { if strings.EqualFold(name, state) { return State(i), nil } } return Unknown, fmt.Errorf("invalid state `%s`", name) } func readFloat(path, filename string) (float64, error) { str, err := os.ReadFile(filepath.Join(path, filename)) if err != nil { return 0, err } if len(str) == 0 { return 0, ErrNotFound } num, err := strconv.ParseFloat(string(str[:len(str)-1]), 64) if err != nil { return 0, err } return num / 1000, nil // Convert micro->milli } func readAmp(path, filename string, volts float64) (float64, error) { val, err := readFloat(path, filename) if err != nil { return 0, err } return val * volts, nil } func isBattery(path string) bool { t, err := os.ReadFile(filepath.Join(path, "type")) return err == nil && string(t) == "Battery\n" } func getBatteryFiles() ([]string, error) { files, err := os.ReadDir(sysfs) if err != nil { return nil, err } var bFiles []string for _, file := range files { path := filepath.Join(sysfs, file.Name()) if isBattery(path) { bFiles = append(bFiles, path) } } if len(bFiles) == 0 { return nil, &NoBatteryError{} } return bFiles, nil } func getByPath(path string) (*battery, error) { b := &battery{} var err error if b.Current, err = readFloat(path, "energy_now"); err == nil { if b.Full, err = readFloat(path, "energy_full"); err != nil { return nil, errors.New("unable to parse energy_full") } } else { currentDoesNotExist := os.IsNotExist(err) if b.Voltage, err = readFloat(path, "voltage_now"); err != nil { return nil, errors.New("unable to parse voltage_now") } b.Voltage /= 1000 if currentDoesNotExist { if b.Current, err = readAmp(path, "charge_now", b.Voltage); err != nil { return nil, errors.New("unable to parse charge_now") } if b.Full, err = readAmp(path, "charge_full", b.Voltage); err != nil { return nil, errors.New("unable to parse charge_full") } } else { if b.Full, err = readFloat(path, "energy_full"); err != nil { return nil, errors.New("unable to parse energy_full") } } } state, err := os.ReadFile(filepath.Join(path, "status")) if err != nil || len(state) == 0 { return nil, errors.New("unable to parse or invalid status") } if b.State, err = newState(string(state[:len(state)-1])); err != nil { return nil, errors.New("unable to map to new state") } return b, nil } func systemGetAll() ([]*battery, error) { bFiles, err := getBatteryFiles() if err != nil { return nil, err } var batteries []*battery var errs Errors for _, bFile := range bFiles { b, err := getByPath(bFile) if err != nil { errs = append(errs, err) continue } batteries = append(batteries, b) } if len(batteries) == 0 { return nil, errs } return batteries, nil } ================================================ FILE: src/runtime/battery/battery_netbsd.go ================================================ package battery import ( "errors" "strconv" "strings" "github.com/jandedobbeleer/oh-my-posh/src/runtime/cmd" ) func Get() (*Info, error) { output, err := cmd.Run("envstat", "-s", "acpibat0:charge", "-n") if err != nil { return nil, err } percentage, err := strconv.Atoi(strings.TrimSpace(output)) if err != nil { return nil, errors.New("unable to parse battery percentage") } return &Info{ Percentage: percentage, State: Unknown, }, nil } ================================================ FILE: src/runtime/battery/battery_openandfreebsd.go ================================================ //go:build openbsd || freebsd package battery import ( "errors" "strconv" "strings" "github.com/jandedobbeleer/oh-my-posh/src/runtime/cmd" ) // See https://man.openbsd.org/man8/apm.8 func mapMostLogicalState(state string) State { switch state { case "3": return Charging case "0", "1": return Discharging case "2": return Empty default: return Unknown } } func parseBatteryOutput(apm_percentage string, apm_status string) (*Info, error) { percentage, err := strconv.Atoi(strings.TrimSpace(apm_percentage)) if err != nil { return nil, errors.New("unable to parse battery percentage") } if percentage == 100 { return &Info{ Percentage: percentage, State: Full, }, nil } return &Info{ Percentage: percentage, State: mapMostLogicalState(apm_status), }, nil } func Get() (*Info, error) { apm_percentage, err := cmd.Run("apm", "-l") if err != nil { return nil, err } apm_status, err := cmd.Run("apm", "-b") if err != nil { return nil, err } return parseBatteryOutput(apm_percentage, apm_status) } ================================================ FILE: src/runtime/battery/battery_openandfreebsd_test.go ================================================ //go:build openbsd || freebsd package battery import ( "testing" "github.com/stretchr/testify/assert" ) func TestParseBatteryOutput(t *testing.T) { cases := []struct { Case string PercentOutput string StatusOutput string ExpectedState State ExpectedPercentage int ExpectError bool }{ { Case: "charging", PercentOutput: "99", StatusOutput: "3", ExpectedState: Charging, ExpectedPercentage: 99, }, { Case: "charging 1%", PercentOutput: "1", StatusOutput: "3", ExpectedState: Charging, ExpectedPercentage: 1, }, { Case: "removed", PercentOutput: "0", StatusOutput: "4", ExpectedState: Unknown, ExpectedPercentage: 0, }, { Case: "charged", PercentOutput: "100", StatusOutput: "0", ExpectedState: Full, ExpectedPercentage: 100, }, { Case: "discharging", PercentOutput: "25", StatusOutput: "1", ExpectedState: Discharging, ExpectedPercentage: 25, }, } for _, tc := range cases { info, err := parseBatteryOutput(tc.PercentOutput, tc.StatusOutput) if tc.ExpectError { assert.Error(t, err, tc.Case) return } assert.Equal(t, tc.ExpectedState, info.State, tc.Case) assert.Equal(t, tc.ExpectedPercentage, info.Percentage, tc.Case) } } ================================================ FILE: src/runtime/battery/battery_windows.go ================================================ // battery // Copyright (C) 2016-2017 Karol 'Kenji Takahashi' Woźniak // // Permission is hereby granted, free of charge, to any person obtaining // a copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included // in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. // IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, // TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE // OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package battery import ( "errors" "syscall" "unsafe" "golang.org/x/sys/windows" ) type batteryQueryInformation struct { BatteryTag uint32 InformationLevel int32 AtRate int32 } type batteryInformation struct { Capabilities uint32 Technology uint8 Reserved [3]uint8 Chemistry [4]uint8 DesignedCapacity uint32 FullChargedCapacity uint32 DefaultAlert1 uint32 DefaultAlert2 uint32 CriticalBias uint32 CycleCount uint32 } type batteryWaitStatus struct { BatteryTag uint32 Timeout uint32 PowerState uint32 LowCapacity uint32 HighCapacity uint32 } type batteryStatus struct { PowerState uint32 Capacity uint32 Voltage uint32 Rate int32 } type guid struct { Data1 uint32 Data2 uint16 Data3 uint16 Data4 [8]byte } type spDeviceInterfaceData struct { cbSize uint32 InterfaceClassGuid guid Flags uint32 Reserved uint } var guidDeviceBattery = guid{ 0x72631e54, 0x78A4, 0x11d0, [8]byte{0xbc, 0xf7, 0x00, 0xaa, 0x00, 0xb7, 0xb3, 0x2a}, } func uint32ToFloat64(num uint32) (float64, error) { if num == 0xffffffff { // BATTERY_UNKNOWN_CAPACITY return 0, errors.New("unknown value received") } return float64(num), nil } func setupDiSetup(proc *windows.LazyProc, args ...uintptr) (uintptr, error) { r1, _, errno := syscall.SyscallN(proc.Addr(), args...) if windows.Handle(r1) == windows.InvalidHandle { if errno != 0 { return 0, error(errno) } return 0, syscall.EINVAL } return r1, nil } func setupDiCall(proc *windows.LazyProc, args ...uintptr) syscall.Errno { r1, _, errno := syscall.SyscallN(proc.Addr(), args...) if r1 == 0 { if errno != 0 { return errno } return syscall.EINVAL } return 0 } var setupapi = &windows.LazyDLL{Name: "setupapi.dll", System: true} var setupDiGetClassDevsW = setupapi.NewProc("SetupDiGetClassDevsW") var setupDiEnumDeviceInterfaces = setupapi.NewProc("SetupDiEnumDeviceInterfaces") var setupDiGetDeviceInterfaceDetailW = setupapi.NewProc("SetupDiGetDeviceInterfaceDetailW") var setupDiDestroyDeviceInfoList = setupapi.NewProc("SetupDiDestroyDeviceInfoList") func readState(powerState uint32) State { switch { case powerState&0x00000004 != 0: return Charging case powerState&0x00000008 != 0: return Empty case powerState&0x00000002 != 0: return Discharging case powerState&0x00000001 != 0: return Full default: return Unknown } } func systemGet(idx int) (*battery, error) { hdev, err := setupDiSetup( setupDiGetClassDevsW, uintptr(unsafe.Pointer(&guidDeviceBattery)), 0, 0, 2|16, // DIGCF_PRESENT|DIGCF_DEVICEINTERFACE ) if err != nil { return nil, err } defer func() { _, _, _ = syscall.SyscallN(setupDiDestroyDeviceInfoList.Addr(), hdev) }() var did spDeviceInterfaceData did.cbSize = uint32(unsafe.Sizeof(did)) errno := setupDiCall( setupDiEnumDeviceInterfaces, hdev, 0, uintptr(unsafe.Pointer(&guidDeviceBattery)), uintptr(idx), uintptr(unsafe.Pointer(&did)), ) if errno == 259 { // ERROR_NO_MORE_ITEMS return nil, ErrNotFound } if errno != 0 { return nil, errno } var cbRequired uint32 errno = setupDiCall( setupDiGetDeviceInterfaceDetailW, hdev, uintptr(unsafe.Pointer(&did)), 0, 0, uintptr(unsafe.Pointer(&cbRequired)), 0, ) if errno != 0 && errno != 122 { // ERROR_INSUFFICIENT_BUFFER return nil, errno } if cbRequired == 0 { return nil, errors.New("no buffer information returned") } // The god damn struct with ANYSIZE_ARRAY of utf16 in it is crazy. // So... let's emulate it with array of uint16 ;-D. // Keep in mind that the first two elements are actually cbSize. didd := make([]uint16, cbRequired/2) cbSize := (*uint32)(unsafe.Pointer(&didd[0])) if unsafe.Sizeof(uint(0)) == 8 { *cbSize = 8 } else { *cbSize = 6 } errno = setupDiCall( setupDiGetDeviceInterfaceDetailW, hdev, uintptr(unsafe.Pointer(&did)), uintptr(unsafe.Pointer(&didd[0])), uintptr(cbRequired), uintptr(unsafe.Pointer(&cbRequired)), 0, ) if errno != 0 { return nil, errno } devicePath := &didd[2:][0] handle, err := windows.CreateFile( devicePath, windows.GENERIC_READ|windows.GENERIC_WRITE, windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE, nil, windows.OPEN_EXISTING, windows.FILE_ATTRIBUTE_NORMAL, 0, ) if err != nil { return nil, err } defer func() { _ = windows.CloseHandle(handle) }() var dwOut uint32 var dwWait uint32 var bqi batteryQueryInformation err = windows.DeviceIoControl( handle, 2703424, // IOCTL_BATTERY_QUERY_TAG (*byte)(unsafe.Pointer(&dwWait)), uint32(unsafe.Sizeof(dwWait)), (*byte)(unsafe.Pointer(&bqi.BatteryTag)), uint32(unsafe.Sizeof(bqi.BatteryTag)), &dwOut, nil, ) if err != nil { return nil, err } if bqi.BatteryTag == 0 { return nil, errors.New("battery tag not returned") } b := &battery{} var bi batteryInformation err = windows.DeviceIoControl( handle, 2703428, // IOCTL_BATTERY_QUERY_INFORMATION (*byte)(unsafe.Pointer(&bqi)), uint32(unsafe.Sizeof(bqi)), (*byte)(unsafe.Pointer(&bi)), uint32(unsafe.Sizeof(bi)), &dwOut, nil, ) if err != nil { return nil, err } b.Full = float64(bi.FullChargedCapacity) bws := batteryWaitStatus{BatteryTag: bqi.BatteryTag} var bs batteryStatus err = windows.DeviceIoControl( handle, 2703436, // IOCTL_BATTERY_QUERY_STATUS (*byte)(unsafe.Pointer(&bws)), uint32(unsafe.Sizeof(bws)), (*byte)(unsafe.Pointer(&bs)), uint32(unsafe.Sizeof(bs)), &dwOut, nil, ) if err != nil { return nil, err } if b.Current, err = uint32ToFloat64(bs.Capacity); err != nil { return nil, err } if b.Voltage, err = uint32ToFloat64(bs.Voltage); err != nil { return nil, err } b.Voltage /= 1000 b.State = readState(bs.PowerState) return b, nil } func systemGetAll() ([]*battery, error) { var batteries []*battery var i int var errs Errors for i = 0; ; i++ { b, err := systemGet(i) if err == ErrNotFound { break } if err != nil { errs = append(errs, err) continue } batteries = append(batteries, b) } if i == 0 { return nil, &NoBatteryError{} } if len(batteries) == 0 { return nil, errs } return batteries, nil } ================================================ FILE: src/runtime/battery/battery_windows_nix.go ================================================ //go:build !darwin && !netbsd && !openbsd && !freebsd package battery import ( "math" ) // battery type represents a single battery entry information. type battery struct { // Current battery state. State State // Current (momentary) capacity (in mWh). Current float64 // Last known full capacity (in mWh). Full float64 // Current voltage (in V). Voltage float64 } func mapMostLogicalState(currentState, newState State) State { switch currentState { case Discharging, NotCharging: return Discharging case Empty: return newState case Charging: if newState == Discharging { return Discharging } return Charging case Unknown: return newState case Full: return newState } return newState } // Get returns information about all batteries in the system. // // If error != nil, it will be either ErrFatal or Errors. // If error is of type Errors, it is guaranteed that length of both returned slices is the same and that i-th error corresponds with i-th battery structure. func Get() (*Info, error) { parseBatteryInfo := func(batteries []*battery) *Info { var info Info var current, total float64 var state State for _, bt := range batteries { current += bt.Current total += bt.Full state = mapMostLogicalState(state, bt.State) } batteryPercentage := current / total * 100 info.Percentage = int(math.Min(100, batteryPercentage)) info.State = state return &info } batteries, err := systemGetAll() if err != nil { return nil, err } return parseBatteryInfo(batteries), nil } ================================================ FILE: src/runtime/battery/battery_windows_nix_test.go ================================================ //go:build !darwin && !netbsd && !openbsd // battery // Copyright (C) 2016-2017 Karol 'Kenji Takahashi' Woźniak // // Permission is hereby granted, free of charge, to any person obtaining // a copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included // in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. // IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, // TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE // OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package battery import ( "testing" "github.com/alecthomas/assert" ) func TestMapBatteriesState(t *testing.T) { cases := []struct { Case string ExpectedState State CurrentState State NewState State }{ {Case: "charging > charged", ExpectedState: Charging, CurrentState: Full, NewState: Charging}, {Case: "charging < discharging", ExpectedState: Discharging, CurrentState: Discharging, NewState: Charging}, {Case: "charging == charging", ExpectedState: Charging, CurrentState: Charging, NewState: Charging}, {Case: "discharging > charged", ExpectedState: Discharging, CurrentState: Full, NewState: Discharging}, {Case: "discharging > unknown", ExpectedState: Discharging, CurrentState: Unknown, NewState: Discharging}, {Case: "discharging > full", ExpectedState: Discharging, CurrentState: Full, NewState: Discharging}, {Case: "discharging > charging 2", ExpectedState: Discharging, CurrentState: Charging, NewState: Discharging}, {Case: "discharging > empty", ExpectedState: Discharging, CurrentState: Empty, NewState: Discharging}, } for _, tc := range cases { assert.Equal(t, tc.ExpectedState, mapMostLogicalState(tc.CurrentState, tc.NewState), tc.Case) } } ================================================ FILE: src/runtime/battery/errors.go ================================================ // battery // Copyright (C) 2016-2017 Karol 'Kenji Takahashi' Woźniak // // Permission is hereby granted, free of charge, to any person obtaining // a copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included // in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. // IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, // TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE // OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package battery import "fmt" var ErrNotFound = fmt.Errorf("not found") type Errors []error func (e Errors) Error() string { var s string for _, err := range e { if err != nil { s += err.Error() + ", " } } // strip trailing colon/space if len(s) > 1 { s = s[:len(s)-2] } return s } ================================================ FILE: src/runtime/battery/errors_test.go ================================================ // battery // Copyright (C) 2016-2017 Karol 'Kenji Takahashi' Woźniak // // Permission is hereby granted, free of charge, to any person obtaining // a copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included // in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. // IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, // TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE // OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package battery import ( "errors" "testing" ) func TestErrors(t *testing.T) { cases := []struct { str string in Errors }{ {"", Errors{nil}}, {"", Errors{errors.New("")}}, {"t1", Errors{errors.New("t1")}}, {"t2, t3", Errors{errors.New("t2"), errors.New("t3")}}, {"t4, t5", Errors{errors.New("t4"), errors.New("t5")}}, } for i, c := range cases { str := c.in.Error() if str != c.str { t.Errorf("%d: %v != %v", i, str, c.str) } } } ================================================ FILE: src/runtime/cmd/run.go ================================================ package cmd import ( "bytes" "context" "os/exec" "strings" runjobs "github.com/jandedobbeleer/oh-my-posh/src/runtime/jobs" ) // Run executes a command while ensuring the OS process is started in its own // process group; the started process is recorded so callers can request a // cleanup (KillGoroutineChildren) if they decide to abort waiting for the // goroutine that spawned it. func Run(command string, args ...string) (string, error) { cmd := exec.CommandContext(context.Background(), command, args...) var out bytes.Buffer var errb bytes.Buffer cmd.Stdout = &out cmd.Stderr = &errb // ensure child runs in its own process group so we can kill the tree if // needed. Implementation is provided by the runtime/jobs package which is // platform aware. runjobs.SetProcessGroup(cmd) if err := cmd.Start(); err != nil { return "", err } // register the started process under the current goroutine runjobs.RegisterProcess(cmd.Process.Pid) defer runjobs.UnregisterProcess(cmd.Process.Pid) if err := cmd.Wait(); err != nil { // Prefer stderr if available output := strings.TrimSpace(errb.String()) if output == "" { output = strings.TrimSpace(out.String()) } return output, err } result := strings.TrimSpace(out.String()) if result == "" { result = strings.TrimSpace(errb.String()) } return result, nil } ================================================ FILE: src/runtime/cmd/run_test.go ================================================ package cmd import ( "testing" runjobs "github.com/jandedobbeleer/oh-my-posh/src/runtime/jobs" ) func TestCurrentGID(t *testing.T) { if gid := runjobs.CurrentGID(); gid == 0 { t.Fatalf("CurrentGID returned 0") } } ================================================ FILE: src/runtime/environment.go ================================================ package runtime import ( "io" "io/fs" "github.com/jandedobbeleer/oh-my-posh/src/runtime/battery" "github.com/jandedobbeleer/oh-my-posh/src/runtime/http" disk "github.com/shirou/gopsutil/v4/disk" ) const ( UNKNOWN = "unknown" WINDOWS = "windows" DARWIN = "darwin" LINUX = "linux" FREEBSD = "freebsd" CMD = "cmd" ANDROID = "android" PRIMARY = "primary" ) type Environment interface { Getenv(key string) string Pwd() string Home() string User() string Root() bool Host() (string, error) GOOS() string Shell() string Platform() string StatusCodes() (int, string) HasFiles(pattern string) bool HasFilesInDir(dir, pattern string) bool HasFolder(folder string) bool HasParentFilePath(input string, followSymlinks bool) (fileInfo *FileInfo, err error) HasFileInParentDirs(pattern string, depth uint) bool ResolveSymlink(input string) (string, error) DirMatchesOneOf(dir string, regexes []string) bool DirIsWritable(input string) bool CommandPath(command string) string HasCommand(command string) bool FileContent(file string) string LsDir(input string) []fs.DirEntry RunCommand(command string, args ...string) (string, error) RunShellCommand(shell, command string) string ExecutionTime() float64 Flags() *Flags BatteryState() (*battery.Info, error) QueryWindowTitles(processName, windowTitleRegex string) (string, error) WindowsRegistryKeyValue(key string) (*WindowsRegistryValue, error) HTTPRequest(url string, body io.Reader, timeout int, requestModifiers ...http.RequestModifier) ([]byte, error) IsWsl() bool IsWsl2() bool IsCygwin() bool StackCount() int TerminalWidth() (int, error) Logs() string InWSLSharedDrive() bool ConvertToLinuxPath(input string) string ConvertToWindowsPath(input string) string Connection(connectionType ConnectionType) (*Connection, error) CursorPosition() (row, col int) SystemInfo() (*SystemInfo, error) } type Flags struct { Type string PipeStatus string ConfigPath string PSWD string Shell string ShellVersion string PWD string AbsolutePWD string ErrorCode int PromptCount int Column int TerminalWidth int ExecutionTime float64 StackCount int ConfigHash uint64 JobCount int HasExtra bool Strict bool Debug bool Cleared bool NoExitCode bool Init bool Migrate bool Eval bool Escape bool IsPrimary bool Plain bool Force bool Streaming bool } type CommandError struct { Err string ExitCode int } func (e *CommandError) Error() string { return e.Err } type FileInfo struct { ParentFolder string Path string IsDir bool } type WindowsRegistryValueType string const ( DWORD = "DWORD" QWORD = "QWORD" BINARY = "BINARY" STRING = "STRING" ) type WindowsRegistryValue struct { ValueType WindowsRegistryValueType String string DWord uint64 QWord uint64 } type NotImplemented struct{} func (n *NotImplemented) Error() string { return "not implemented" } type ConnectionType string const ( ETHERNET ConnectionType = "ethernet" WIFI ConnectionType = "wifi" CELLULAR ConnectionType = "cellular" BLUETOOTH ConnectionType = "bluetooth" ) type Connection struct { Name string Type ConnectionType SSID string TransmitRate uint64 ReceiveRate uint64 } type Memory struct { PhysicalTotalMemory uint64 PhysicalAvailableMemory uint64 PhysicalFreeMemory uint64 PhysicalPercentUsed float64 SwapTotalMemory uint64 SwapFreeMemory uint64 SwapPercentUsed float64 } type SystemInfo struct { Disks map[string]disk.IOCountersStat Memory Load1 float64 Load5 float64 Load15 float64 } ================================================ FILE: src/runtime/http/connection.go ================================================ //revive:disable:var-naming // package intentionally mirrors standard name for compatibility across runtime package http import ( "context" "net" "time" ) // IsConnected checks if we can connect to ohmyposh within 200ms // If we can connect, we are connected; otherwise, let's consider being offline func IsConnected() bool { timeout := 200 * time.Millisecond dialer := &net.Dialer{ Timeout: timeout, } ctx := context.Background() conn, err := dialer.DialContext(ctx, "tcp", "ohmyposh.dev:80") if err != nil { return false } conn.Close() return true } ================================================ FILE: src/runtime/http/download.go ================================================ //revive:disable:var-naming // package intentionally mirrors standard name for compatibility across runtime package http import ( "context" "fmt" "io" httplib "net/http" "strings" "time" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/log" ) func Download(url string, isCacheEnabled bool) ([]byte, error) { defer log.Trace(time.Now(), url) // some users use the blob url, we need to convert it to the raw url themeBlob := "https://github.com/JanDeDobbeleer/oh-my-posh/blob/main/themes/" url = strings.Replace(url, themeBlob, "https://raw.githubusercontent.com/JanDeDobbeleer/oh-my-posh/main/themes/", 1) ctx, cncl := context.WithTimeout(context.Background(), time.Second*time.Duration(5)) defer cncl() request, err := httplib.NewRequestWithContext(ctx, httplib.MethodGet, url, nil) if err != nil { log.Error(err) return nil, err } request.Header.Add("User-Agent", "oh-my-posh") // if we have an etag, add it to the request to check if the file changed etag, OK := cache.Get[string](cache.Device, etagKey(url)) if OK { log.Debugf("found etag in cache: %s", etag) request.Header.Set("If-None-Match", etag) } cachedData := func() ([]byte, error) { cachedData, OK := cache.Get[[]byte](cache.Device, dataKey(url)) if OK { return cachedData, nil } return nil, fmt.Errorf("resource not modified but no cached data found") } response, err := HTTPClient.Do(request) if err != nil { log.Error(err) return cachedData() } defer response.Body.Close() if response.StatusCode == httplib.StatusNotModified { log.Debug("resource not modified, using cached version") return cachedData() } if response.StatusCode != httplib.StatusOK { err := fmt.Errorf("status code: %d", response.StatusCode) log.Error(err) return cachedData() } etag = response.Header.Get("ETag") if etag != "" && isCacheEnabled { cache.Set(cache.Device, etagKey(url), etag, cache.INFINITE) } data, err := io.ReadAll(response.Body) if err != nil { log.Error(err) return cachedData() } if isCacheEnabled { cache.Set(cache.Device, dataKey(url), data, cache.INFINITE) } return data, nil } func etagKey(url string) string { return fmt.Sprintf("%s.etag", url) } func dataKey(url string) string { return fmt.Sprintf("%s.data", url) } ================================================ FILE: src/runtime/http/http.go ================================================ //revive:disable:var-naming // package intentionally mirrors standard name for compatibility across runtime package http import ( "net" "net/http" "time" ) // Inspired by: https://www.thegreatcodeadventure.com/mocking-http-requests-in-golang/ type httpClient interface { Do(req *http.Request) (*http.Response, error) } var ( defaultTransport http.RoundTripper = &http.Transport{ Proxy: http.ProxyFromEnvironment, Dial: (&net.Dialer{ Timeout: 10 * time.Second, }).Dial, TLSHandshakeTimeout: 10 * time.Second, ResponseHeaderTimeout: 10 * time.Second, } HTTPClient httpClient = &http.Client{Transport: defaultTransport} ) type Error struct { StatusCode int } func (e *Error) Error() string { return http.StatusText(e.StatusCode) } ================================================ FILE: src/runtime/http/oauth.go ================================================ //revive:disable:var-naming // package intentionally mirrors standard name for compatibility across runtime package http import ( "encoding/json" "fmt" "io" httplib "net/http" "github.com/jandedobbeleer/oh-my-posh/src/cache" ) const ( Timeout = "timeout" InvalidRefreshToken = "invalid refresh token" TokenRefreshFailed = "token refresh error" DefaultRefreshToken = "111111111111111111111111111111" ) type tokenExchange struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` ExpiresIn int `json:"expires_in"` } type OAuthError struct { message string } func (a *OAuthError) Error() string { return a.message } type OAuthRequest struct { AccessTokenKey string RefreshTokenKey string SegmentName string RefreshToken string AccessToken string Request } func (o *OAuthRequest) getAccessToken() (string, error) { // get directly from cache if accessToken, OK := cache.Get[string](cache.Device, o.AccessTokenKey); OK && len(accessToken) != 0 { return accessToken, nil } // use cached refresh token to get new access token if refreshToken, OK := cache.Get[string](cache.Device, o.RefreshTokenKey); OK && len(refreshToken) != 0 { if accessToken, err := o.refreshToken(refreshToken); err == nil { return accessToken, nil } } // use initial refresh token from property // refreshToken := o.props.GetString(options.RefreshToken, "") // ignore an empty or default refresh token if o.RefreshToken == "" || o.RefreshToken == DefaultRefreshToken { return "", &OAuthError{ message: InvalidRefreshToken, } } // no need to let the user provide access token, we'll always verify the refresh token accessToken, err := o.refreshToken(o.RefreshToken) return accessToken, err } func (o *OAuthRequest) refreshToken(refreshToken string) (string, error) { if o.HTTPTimeout == 0 { o.HTTPTimeout = 20 } url := fmt.Sprintf("https://ohmyposh.dev/api/refresh?segment=%s&token=%s", o.SegmentName, refreshToken) body, err := o.Env.HTTPRequest(url, nil, o.HTTPTimeout) if err != nil { return "", &OAuthError{ // This might happen if /api was asleep. Assume the user will just retry message: Timeout, } } tokens := &tokenExchange{} err = json.Unmarshal(body, &tokens) if err != nil { return "", &OAuthError{ message: TokenRefreshFailed, } } // add tokens to cache cache.Set(cache.Device, o.AccessTokenKey, tokens.AccessToken, cache.ToDuration(tokens.ExpiresIn)) cache.Set(cache.Device, o.RefreshTokenKey, tokens.RefreshToken, cache.TWOYEARS) return tokens.AccessToken, nil } func OauthResult[a any](o *OAuthRequest, url string, body io.Reader, requestModifiers ...RequestModifier) (a, error) { accessToken, err := o.getAccessToken() if err != nil { var data a return data, err } // add token to header for authentication addAuthHeader := func(request *httplib.Request) { request.Header.Add("Authorization", "Bearer "+accessToken) } if requestModifiers == nil { requestModifiers = []RequestModifier{} } requestModifiers = append(requestModifiers, addAuthHeader) return Do[a](&o.Request, url, body, requestModifiers...) } ================================================ FILE: src/runtime/http/oauth_test.go ================================================ //revive:disable:var-naming // test package matches implementation; lint warning is intentional package http import ( "fmt" "testing" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/stretchr/testify/assert" ) type data struct { Hello string `json:"hello"` } func TestOauthResult(t *testing.T) { accessTokenKey := "test_access_token" refreshTokenKey := "test_refresh_token" tokenResponse := `{ "access_token":"NEW_ACCESSTOKEN","refresh_token":"NEW_REFRESHTOKEN", "expires_in":1234 }` jsonResponse := `{ "hello":"world" }` successData := &data{Hello: "world"} cases := []struct { Error error ExpectedData *data AccessToken string RefreshToken string TokenResponse string JSONResponse string CacheJSONResponse string Case string ExpectedErrorMessage string CacheTimeout int ResponseCacheMiss bool AccessTokenFromCache bool RefreshTokenFromCache bool }{ { Case: "No initial tokens", ExpectedErrorMessage: InvalidRefreshToken, }, { Case: "Use config tokens", AccessToken: "INITIAL_ACCESSTOKEN", RefreshToken: "INITIAL_REFRESHTOKEN", TokenResponse: tokenResponse, JSONResponse: jsonResponse, ExpectedData: successData, }, { Case: "Access token from cache", AccessToken: "ACCESSTOKEN", AccessTokenFromCache: true, JSONResponse: jsonResponse, ExpectedData: successData, }, { Case: "Refresh token from cache", RefreshToken: "REFRESH_TOKEN", RefreshTokenFromCache: true, JSONResponse: jsonResponse, TokenResponse: tokenResponse, ExpectedData: successData, }, { Case: "Refresh token from cache, success", RefreshToken: "REFRESH_TOKEN", RefreshTokenFromCache: true, JSONResponse: jsonResponse, TokenResponse: tokenResponse, ExpectedData: successData, }, { Case: "Refresh API error", RefreshToken: "REFRESH_TOKEN", RefreshTokenFromCache: true, Error: fmt.Errorf("API error"), ExpectedErrorMessage: Timeout, }, { Case: "Refresh API parse error", RefreshToken: "REFRESH_TOKEN", RefreshTokenFromCache: true, TokenResponse: "INVALID_JSON", ExpectedErrorMessage: TokenRefreshFailed, }, { Case: "Default config token", RefreshToken: DefaultRefreshToken, ExpectedErrorMessage: InvalidRefreshToken, }, { Case: "Cache data, invalid data", RefreshToken: "REFRESH_TOKEN", TokenResponse: tokenResponse, JSONResponse: jsonResponse, ExpectedData: successData, }, { Case: "Cache data, no cache", RefreshToken: "REFRESH_TOKEN", TokenResponse: tokenResponse, JSONResponse: jsonResponse, ExpectedData: successData, }, { Case: "API body failure", AccessToken: "ACCESSTOKEN", AccessTokenFromCache: true, JSONResponse: "ERR", ExpectedErrorMessage: "invalid character 'E' looking for beginning of value", }, { Case: "API request failure", AccessToken: "ACCESSTOKEN", AccessTokenFromCache: true, JSONResponse: "ERR", Error: fmt.Errorf("no response"), ExpectedErrorMessage: "no response", }, } for _, tc := range cases { url := "https://www.strava.com/api/v3/athlete/activities?page=1&per_page=1" tokenURL := fmt.Sprintf("https://ohmyposh.dev/api/refresh?segment=test&token=%s", tc.RefreshToken) if tc.AccessTokenFromCache { cache.Set(cache.Device, accessTokenKey, tc.AccessToken, cache.INFINITE) } if tc.RefreshTokenFromCache { cache.Set(cache.Device, refreshTokenKey, tc.RefreshToken, cache.INFINITE) } env := &MockedEnvironment{} env.On("HTTPRequest", url).Return([]byte(tc.JSONResponse), tc.Error) env.On("HTTPRequest", tokenURL).Return([]byte(tc.TokenResponse), tc.Error) oauth := &OAuthRequest{ AccessTokenKey: accessTokenKey, RefreshTokenKey: refreshTokenKey, SegmentName: "test", AccessToken: tc.AccessToken, RefreshToken: tc.RefreshToken, Request: Request{ Env: env, HTTPTimeout: 20, }, } got, err := OauthResult[*data](oauth, url, nil) assert.Equal(t, tc.ExpectedData, got, tc.Case) if tc.ExpectedErrorMessage == "" { assert.Nil(t, err, tc.Case) } else { assert.Equal(t, tc.ExpectedErrorMessage, err.Error(), tc.Case) } cache.DeleteAll(cache.Device) } } ================================================ FILE: src/runtime/http/request.go ================================================ //revive:disable:var-naming // package intentionally mirrors standard name for compatibility across runtime package http import ( "encoding/json" "io" "net/http" "github.com/jandedobbeleer/oh-my-posh/src/log" ) type RequestModifier func(request *http.Request) type Request struct { Env Environment HTTPTimeout int } type Environment interface { HTTPRequest(url string, body io.Reader, timeout int, requestModifiers ...RequestModifier) ([]byte, error) } func Do[a any](r *Request, url string, body io.Reader, requestModifiers ...RequestModifier) (a, error) { var data a httpTimeout := r.HTTPTimeout // r.props.GetInt(options.HTTPTimeout, options.DefaultHTTPTimeout) responseBody, err := r.Env.HTTPRequest(url, body, httpTimeout, requestModifiers...) if err != nil { log.Error(err) return data, err } err = json.Unmarshal(responseBody, &data) if err != nil { log.Error(err) return data, err } return data, nil } ================================================ FILE: src/runtime/http/request_test.go ================================================ //revive:disable:var-naming // test package matches implementation; lint warning is intentional package http import ( "io" "net" "testing" "github.com/stretchr/testify/assert" testify_ "github.com/stretchr/testify/mock" ) type MockedEnvironment struct { testify_.Mock } func (env *MockedEnvironment) HTTPRequest(url string, _ io.Reader, _ int, _ ...RequestModifier) ([]byte, error) { args := env.Called(url) return args.Get(0).([]byte), args.Error(1) } func TestRequestResult(t *testing.T) { successData := &data{Hello: "world"} jsonResponse := `{ "hello":"world" }` url := "https://google.com?q=hello" cases := []struct { Error error ExpectedData *data Case string JSONResponse string CacheJSONResponse string ExpectedErrorMessage string CacheTimeout int ResponseCacheMiss bool }{ { Case: "No cache", JSONResponse: jsonResponse, ExpectedData: successData, }, { Case: "DNS error", Error: &net.DNSError{IsNotFound: true}, ExpectedErrorMessage: "lookup : ", }, { Case: "Response incorrect", JSONResponse: `[`, ExpectedErrorMessage: "unexpected end of JSON input", }, } for _, tc := range cases { env := &MockedEnvironment{} env.On("HTTPRequest", url).Return([]byte(tc.JSONResponse), tc.Error) request := &Request{ Env: env, HTTPTimeout: 0, } got, err := Do[*data](request, url, nil) assert.Equal(t, tc.ExpectedData, got, tc.Case) if tc.ExpectedErrorMessage == "" { assert.Nil(t, err, tc.Case) } else { assert.Equal(t, tc.ExpectedErrorMessage, err.Error(), tc.Case) } } } ================================================ FILE: src/runtime/jobs/jobs_common.go ================================================ package jobs import ( "runtime" "strconv" "strings" ) // CurrentGID returns the current goroutine's id. We expose this here so // callers can register PIDs without parsing runtime.Stack in multiple // places. func CurrentGID() uint64 { buf := make([]byte, 64) n := runtime.Stack(buf, false) s := strings.Fields(strings.TrimPrefix(string(buf[:n]), "goroutine ")) if len(s) == 0 { return 0 } idStr := s[0] id, err := strconv.ParseUint(idStr, 10, 64) if err != nil { return 0 } return id } ================================================ FILE: src/runtime/jobs/jobs_other.go ================================================ //go:build !windows package jobs import ( "fmt" "os/exec" "strings" "sync" "syscall" ) var ( processesMu sync.Mutex processes = map[uint64]map[int]struct{}{} ) func CreateJobForGoroutine(_ string) error { return nil } func AssignPidToGoroutineJob(_ int) error { return nil } // setProcessGroup ensures the child process runs in its own process group so // it can be killed with a group kill (negative pid). func SetProcessGroup(cmd *exec.Cmd) { cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} } // registerProcessWithGID keeps track of a started child process for the // given goroutine id. func RegisterProcess(pid int) { gid := CurrentGID() processesMu.Lock() m := processes[gid] if m == nil { m = map[int]struct{}{} processes[gid] = m } m[pid] = struct{}{} processesMu.Unlock() } func UnregisterProcess(pid int) { gid := CurrentGID() processesMu.Lock() if m, ok := processes[gid]; ok { delete(m, pid) if len(m) == 0 { delete(processes, gid) } } processesMu.Unlock() } // KillGoroutineChildren attempts to kill all child processes started by the // goroutine identified by gid using process groups (PGID). This mirrors the // previous behavior performed in runtime/cmd. func KillGoroutineChildren(gid uint64) error { processesMu.Lock() pidsMap, ok := processes[gid] if !ok || len(pidsMap) == 0 { processesMu.Unlock() return nil } pids := make([]int, 0, len(pidsMap)) for pid := range pidsMap { pids = append(pids, pid) } delete(processes, gid) processesMu.Unlock() var errs []string for _, pid := range pids { // negative pid kills the process group if err := syscall.Kill(-pid, syscall.SIGKILL); err != nil { errs = append(errs, fmt.Sprintf("kill -%d: %v", pid, err)) } } if len(errs) > 0 { return fmt.Errorf("failed to kill child processes: %s", strings.Join(errs, "; ")) } return nil } ================================================ FILE: src/runtime/jobs/jobs_windows.go ================================================ //go:build windows package jobs import ( "context" "fmt" "os/exec" "strconv" "strings" "sync" "syscall" "time" "unsafe" "github.com/jandedobbeleer/oh-my-posh/src/log" "golang.org/x/sys/windows" ) var ( jobsMu sync.Mutex jobs = map[uint64]windows.Handle{} processesMu sync.Mutex processes = map[uint64]map[int]struct{}{} ) // CreateJobForGoroutine creates a Job object for gid and sets the // JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE flag so closing/terminating the job // kills all assigned processes. func CreateJobForGoroutine(label string) error { gid := CurrentGID() defer log.Trace(time.Now(), fmt.Sprintf("creating job for goroutine(%s): %d", label, gid)) jobsMu.Lock() if _, ok := jobs[gid]; ok { jobsMu.Unlock() return nil } jobsMu.Unlock() job, err := windows.CreateJobObject(nil, nil) if err != nil { return err } var info windows.JOBOBJECT_EXTENDED_LIMIT_INFORMATION info.BasicLimitInformation.LimitFlags = windows.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE size := uint32(unsafe.Sizeof(info)) if _, err := windows.SetInformationJobObject(job, windows.JobObjectExtendedLimitInformation, uintptr(unsafe.Pointer(&info)), size); err != nil { _ = windows.CloseHandle(job) } jobsMu.Lock() jobs[gid] = job jobsMu.Unlock() return nil } // registerProcessWithGID keeps track of a started child process for the // given goroutine id and attempts to assign it to the Job object if present. func RegisterProcess(pid int) { gid := CurrentGID() processesMu.Lock() m := processes[gid] if m == nil { m = map[int]struct{}{} processes[gid] = m } m[pid] = struct{}{} processesMu.Unlock() // Try to assign to job if exists (best-effort) jobsMu.Lock() job, ok := jobs[gid] jobsMu.Unlock() if !ok { log.Debugf("no job found for goroutine %d when assigning pid %d", gid, pid) return } proc, err := windows.OpenProcess(windows.PROCESS_SET_QUOTA|windows.PROCESS_TERMINATE, false, uint32(pid)) if err != nil { log.Error(err) return } defer func() { err = windows.CloseHandle(proc) if err != nil { log.Error(err) } }() if err = windows.AssignProcessToJobObject(job, proc); err != nil { log.Error(err) } log.Debugf("successfully added process to job for goroutine: %d, pid: %d", gid, pid) } func UnregisterProcess(pid int) { gid := CurrentGID() processesMu.Lock() if m, ok := processes[gid]; ok { delete(m, pid) if len(m) == 0 { delete(processes, gid) } } processesMu.Unlock() } // KillGoroutineChildren will first try to terminate a Job if present, and // otherwise will fall back to taskkill for each recorded pid. func KillGoroutineChildren(gid uint64) error { // if Job exists, prefer terminating the Job jobsMu.Lock() job, hasJob := jobs[gid] if hasJob { delete(jobs, gid) } jobsMu.Unlock() if hasJob { // Terminate the job which kills all processes in it if err := windows.TerminateJobObject(job, 1); err == nil { // cleanup recorded pids as well processesMu.Lock() delete(processes, gid) processesMu.Unlock() log.Debugf("successfully terminated job object for goroutine: %d", gid) return nil } } // No job or terminate failed; fall back to per-pid taskkill processesMu.Lock() pidsMap, ok := processes[gid] if !ok || len(pidsMap) == 0 { processesMu.Unlock() return nil } pids := make([]int, 0, len(pidsMap)) for pid := range pidsMap { pids = append(pids, pid) } delete(processes, gid) processesMu.Unlock() var errs []string for _, pid := range pids { if err := exec.CommandContext(context.Background(), "taskkill", "/T", "/F", "/PID", strconv.Itoa(pid)).Run(); err != nil { errs = append(errs, fmt.Sprintf("taskkill %d: %v", pid, err)) } } if len(errs) > 0 { return fmt.Errorf("failed to kill child processes: %s", strings.Join(errs, "; ")) } return nil } // setProcessGroup ensures the child process runs in its own process group // (CREATE_NEW_PROCESS_GROUP) so it can be terminated as a group. func SetProcessGroup(cmd *exec.Cmd) { cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP} } ================================================ FILE: src/runtime/mock/environment.go ================================================ package mock import ( "io" "io/fs" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/runtime/battery" "github.com/jandedobbeleer/oh-my-posh/src/runtime/http" mock "github.com/stretchr/testify/mock" ) type Environment struct { mock.Mock } func (env *Environment) Getenv(key string) string { args := env.Called(key) return args.String(0) } func (env *Environment) Pwd() string { args := env.Called() return args.String(0) } func (env *Environment) Home() string { args := env.Called() return args.String(0) } func (env *Environment) HasFiles(pattern string) bool { args := env.Called(pattern) return args.Bool(0) } func (env *Environment) HasFilesInDir(dir, pattern string) bool { args := env.Called(dir, pattern) return args.Bool(0) } func (env *Environment) HasFolder(folder string) bool { args := env.Called(folder) return args.Bool(0) } func (env *Environment) ResolveSymlink(input string) (string, error) { args := env.Called(input) return args.String(0), args.Error(1) } func (env *Environment) FileContent(file string) string { args := env.Called(file) return args.String(0) } func (env *Environment) LsDir(input string) []fs.DirEntry { args := env.Called(input) return args.Get(0).([]fs.DirEntry) } func (env *Environment) User() string { args := env.Called() return args.String(0) } func (env *Environment) Host() (string, error) { args := env.Called() return args.String(0), args.Error(1) } func (env *Environment) GOOS() string { args := env.Called() return args.String(0) } func (env *Environment) Platform() string { args := env.Called() return args.String(0) } func (env *Environment) CommandPath(command string) string { args := env.Called(command) return args.String(0) } func (env *Environment) HasCommand(command string) bool { args := env.Called(command) return args.Bool(0) } func (env *Environment) RunCommand(command string, args ...string) (string, error) { arguments := env.Called(command, args) return arguments.String(0), arguments.Error(1) } func (env *Environment) RunShellCommand(shell, command string) string { args := env.Called(shell, command) return args.String(0) } func (env *Environment) StatusCodes() (int, string) { args := env.Called() return args.Int(0), args.String(1) } func (env *Environment) ExecutionTime() float64 { args := env.Called() return float64(args.Int(0)) } func (env *Environment) Root() bool { args := env.Called() return args.Bool(0) } func (env *Environment) Flags() *runtime.Flags { arguments := env.Called() return arguments.Get(0).(*runtime.Flags) } func (env *Environment) BatteryState() (*battery.Info, error) { args := env.Called() return args.Get(0).(*battery.Info), args.Error(1) } func (env *Environment) Shell() string { args := env.Called() return args.String(0) } func (env *Environment) QueryWindowTitles(processName, windowTitleRegex string) (string, error) { args := env.Called(processName, windowTitleRegex) return args.String(0), args.Error(1) } func (env *Environment) WindowsRegistryKeyValue(path string) (*runtime.WindowsRegistryValue, error) { args := env.Called(path) return args.Get(0).(*runtime.WindowsRegistryValue), args.Error(1) } func (env *Environment) HTTPRequest(url string, _ io.Reader, _ int, _ ...http.RequestModifier) ([]byte, error) { args := env.Called(url) return args.Get(0).([]byte), args.Error(1) } func (env *Environment) HasParentFilePath(parent string, followSymlinks bool) (*runtime.FileInfo, error) { args := env.Called(parent, followSymlinks) return args.Get(0).(*runtime.FileInfo), args.Error(1) } func (env *Environment) StackCount() int { args := env.Called() return args.Int(0) } func (env *Environment) IsWsl() bool { args := env.Called() return args.Bool(0) } func (env *Environment) IsWsl2() bool { args := env.Called() return args.Bool(0) } func (env *Environment) IsCygwin() bool { args := env.Called() return args.Bool(0) } func (env *Environment) TerminalWidth() (int, error) { args := env.Called() return args.Int(0), args.Error(1) } func (env *Environment) CachePath() string { args := env.Called() return args.String(0) } func (env *Environment) Close() { _ = env.Called() } func (env *Environment) Logs() string { args := env.Called() return args.String(0) } func (env *Environment) InWSLSharedDrive() bool { args := env.Called() return args.Bool(0) } func (env *Environment) ConvertToWindowsPath(input string) string { args := env.Called(input) return args.String(0) } func (env *Environment) ConvertToLinuxPath(_ string) string { args := env.Called() return args.String(0) } func (env *Environment) Connection(connectionType runtime.ConnectionType) (*runtime.Connection, error) { args := env.Called(connectionType) return args.Get(0).(*runtime.Connection), args.Error(1) } func (env *Environment) MockGitCommand(dir, returnValue string, args ...string) { args = append([]string{"-C", dir, "--no-optional-locks", "-c", "core.quotepath=false", "-c", "color.status=false"}, args...) env.On("RunCommand", "git", args).Return(returnValue, nil) } func (env *Environment) MockHgCommand(dir, returnValue string, args ...string) { args = append([]string{"-R", dir}, args...) env.On("RunCommand", "hg", args).Return(returnValue, nil) } func (env *Environment) MockJjCommand(dir, returnValue string, args ...string) { args = append([]string{"--repository", dir, "--no-pager", "--color", "never", "--ignore-working-copy"}, args...) env.On("RunCommand", "jj", args).Return(returnValue, nil) } func (env *Environment) MockSvnCommand(dir, returnValue string, args ...string) { args = append([]string{"-C", dir, "--no-optional-locks", "-c", "core.quotepath=false", "-c", "color.status=false"}, args...) env.On("RunCommand", "svn", args).Return(returnValue, nil) } func (env *Environment) HasFileInParentDirs(pattern string, depth uint) bool { args := env.Called(pattern, depth) return args.Bool(0) } func (env *Environment) DirMatchesOneOf(dir string, regexes []string) bool { args := env.Called(dir, regexes) return args.Bool(0) } func (env *Environment) DirIsWritable(path string) bool { args := env.Called(path) return args.Bool(0) } func (env *Environment) CursorPosition() (int, int) { args := env.Called() return args.Int(0), args.Int(1) } func (env *Environment) SystemInfo() (*runtime.SystemInfo, error) { args := env.Called() return args.Get(0).(*runtime.SystemInfo), args.Error(1) } func (env *Environment) Unset(name string) { for i := 0; i < len(env.ExpectedCalls); i++ { f := env.ExpectedCalls[i] if f.Method == name { f.Unset() } } } ================================================ FILE: src/runtime/networks_windows.go ================================================ package runtime import ( "errors" "strings" "syscall" "time" "unsafe" "github.com/jandedobbeleer/oh-my-posh/src/log" "golang.org/x/sys/windows" ) var ( wlanapi = syscall.NewLazyDLL("wlanapi.dll") hWlanOpenHandle = wlanapi.NewProc("WlanOpenHandle") hWlanCloseHandle = wlanapi.NewProc("WlanCloseHandle") hWlanQueryInterface = wlanapi.NewProc("WlanQueryInterface") hWlanEnumInterfaces = wlanapi.NewProc("WlanEnumInterfaces") ) type MIN_IF_TABLE2 struct { NumEntries uint64 Table [256]MIB_IF_ROW2 } const ( IF_MAX_STRING_SIZE uint64 = 256 IF_MAX_PHYS_ADDRESS_LENGTH uint64 = 32 ) type MIB_IF_ROW2 struct { InterfaceLuid uint64 InterfaceIndex uint32 InterfaceGUID windows.GUID Alias [IF_MAX_STRING_SIZE + 1]uint16 Description [IF_MAX_STRING_SIZE + 1]uint16 PhysicalAddressLength uint32 PhysicalAddress [IF_MAX_PHYS_ADDRESS_LENGTH]uint8 PermanentPhysicalAddress [IF_MAX_PHYS_ADDRESS_LENGTH]uint8 Mtu uint32 Type uint32 TunnelType uint32 MediaType uint32 PhysicalMediumType uint32 AccessType uint32 DirectionType uint32 InterfaceAndOperStatusFlags struct { HardwareInterface bool FilterInterface bool ConnectorPresent bool NotAuthenticated bool NotMediaConnected bool Paused bool LowPower bool EndPointInterface bool } OperStatus uint32 AdminStatus uint32 MediaConnectState uint32 NetworkGUID windows.GUID ConnectionType uint32 TransmitLinkSpeed uint64 ReceiveLinkSpeed uint64 InOctets uint64 InUcastPkts uint64 InNUcastPkts uint64 InDiscards uint64 InErrors uint64 InUnknownProtos uint64 InUcastOctets uint64 InMulticastOctets uint64 InBroadcastOctets uint64 OutOctets uint64 OutUcastPkts uint64 OutNUcastPkts uint64 OutDiscards uint64 OutErrors uint64 OutUcastOctets uint64 OutMulticastOctets uint64 OutBroadcastOctets uint64 OutQLen uint64 } //nolint:unused type WLAN_INTERFACE_INFO_LIST struct { dwNumberOfItems uint32 dwIndex uint32 InterfaceInfo [1]WLAN_INTERFACE_INFO } type WLAN_INTERFACE_INFO struct { InterfaceGuid syscall.GUID strInterfaceDescription [256]uint16 isState uint32 } const ( WLAN_MAX_NAME_LENGTH int64 = 256 DOT11_SSID_MAX_LENGTH int64 = 32 ) //nolint:unused type WLAN_CONNECTION_ATTRIBUTES struct { isState uint32 wlanConnectionMode uint32 strProfileName [WLAN_MAX_NAME_LENGTH]uint16 wlanAssociationAttributes WLAN_ASSOCIATION_ATTRIBUTES wlanSecurityAttributes WLAN_SECURITY_ATTRIBUTES } //nolint:unused type WLAN_ASSOCIATION_ATTRIBUTES struct { dot11Ssid DOT11_SSID dot11BssType uint32 dot11Bssid [6]uint8 dot11PhyType uint32 uDot11PhyIndex uint32 wlanSignalQuality uint32 ulRxRate uint32 ulTxRate uint32 } //nolint:unused type WLAN_SECURITY_ATTRIBUTES struct { bSecurityEnabled uint32 bOneXEnabled uint32 dot11AuthAlgorithm uint32 dot11CipherAlgorithm uint32 } type DOT11_SSID struct { uSSIDLength uint32 ucSSID [DOT11_SSID_MAX_LENGTH]uint8 } func (term *Terminal) getConnections() []*Connection { var pIFTable2 *MIN_IF_TABLE2 _, _, _ = hGetIfTable2.Call(uintptr(unsafe.Pointer(&pIFTable2))) networks := make([]*Connection, 0) for i := 0; i < int(pIFTable2.NumEntries); i++ { networkInterface := pIFTable2.Table[i] alias := strings.TrimRight(syscall.UTF16ToString(networkInterface.Alias[:]), "\x00") if networkInterface.OperStatus != 1 || // not connected or functional !networkInterface.InterfaceAndOperStatusFlags.HardwareInterface || // rule out software interfaces strings.HasPrefix(alias, "Local Area Connection") || // not relevant strings.Index(alias, "-") >= 3 { // rule out parts of Ethernet filter interfaces // e.g. : "Ethernet-WFP Native MAC Layer LightWeight Filter-0000" continue } var connectionType ConnectionType var ssid string switch networkInterface.Type { case 6: connectionType = ETHERNET case 237, 234, 244: connectionType = CELLULAR } if networkInterface.PhysicalMediumType == 10 { connectionType = BLUETOOTH } // skip connections which aren't relevant if connectionType == "" { continue } log.Debugf("Found network interface: %s", alias) network := &Connection{ Type: connectionType, Name: alias, TransmitRate: networkInterface.TransmitLinkSpeed, ReceiveRate: networkInterface.ReceiveLinkSpeed, SSID: ssid, } networks = append(networks, network) } wifi, err := term.wifiNetwork() if err == nil { networks = append(networks, wifi) return networks } log.Error(err) return networks } func (term *Terminal) wifiNetwork() (*Connection, error) { defer log.Trace(time.Now()) // Open handle var pdwNegotiatedVersion uint32 var phClientHandle uint32 e, _, err := hWlanOpenHandle.Call(uintptr(uint32(2)), uintptr(unsafe.Pointer(nil)), uintptr(unsafe.Pointer(&pdwNegotiatedVersion)), uintptr(unsafe.Pointer(&phClientHandle))) if e != 0 { return nil, err } defer func() { _, _, _ = hWlanCloseHandle.Call(uintptr(phClientHandle), uintptr(unsafe.Pointer(nil))) }() // list interfaces var interfaceList *WLAN_INTERFACE_INFO_LIST e, _, err = hWlanEnumInterfaces.Call(uintptr(phClientHandle), uintptr(unsafe.Pointer(nil)), uintptr(unsafe.Pointer(&interfaceList))) if e != 0 { return nil, err } // use first interface that is connected numberOfInterfaces := int(interfaceList.dwNumberOfItems) infoSize := unsafe.Sizeof(interfaceList.InterfaceInfo[0]) for i := range numberOfInterfaces { network := (*WLAN_INTERFACE_INFO)(unsafe.Add(unsafe.Pointer(&interfaceList.InterfaceInfo[0]), uintptr(i)*infoSize)) if network.isState != 1 { log.Debug("Skipping non-connected wifi interface") continue } return term.parseNetworkInterface(network, phClientHandle) } return nil, errors.New("not connected") } func (term *Terminal) parseNetworkInterface(network *WLAN_INTERFACE_INFO, clientHandle uint32) (*Connection, error) { info := Connection{ Type: WIFI, } // Query wifi connection state var dataSize uint32 var wlanAttr *WLAN_CONNECTION_ATTRIBUTES e, _, err := hWlanQueryInterface.Call(uintptr(clientHandle), uintptr(unsafe.Pointer(&network.InterfaceGuid)), uintptr(7), // wlan_intf_opcode_current_connection uintptr(unsafe.Pointer(nil)), uintptr(unsafe.Pointer(&dataSize)), uintptr(unsafe.Pointer(&wlanAttr)), uintptr(unsafe.Pointer(nil))) if e != 0 { return &info, err } // SSID ssid := wlanAttr.wlanAssociationAttributes.dot11Ssid if ssid.uSSIDLength > 0 { info.SSID = string(ssid.ucSSID[0:ssid.uSSIDLength]) info.Name = info.SSID log.Debugf("Found wifi interface: %s", info.SSID) } info.TransmitRate = uint64(wlanAttr.wlanAssociationAttributes.ulTxRate / 1024) info.ReceiveRate = uint64(wlanAttr.wlanAssociationAttributes.ulRxRate / 1024) return &info, nil } ================================================ FILE: src/runtime/path/clean.go ================================================ package path import ( "fmt" "path/filepath" "runtime" "strings" "github.com/jandedobbeleer/oh-my-posh/src/regex" "github.com/jandedobbeleer/oh-my-posh/src/text" ) // Base returns the last element of path. // Trailing path separators are removed before extracting the last element. // If the path consists entirely of separators, Base returns a single separator. func Base(input string) string { volumeName := filepath.VolumeName(input) // Strip trailing slashes. for len(input) > 0 && IsSeparator(input[len(input)-1]) { input = input[0 : len(input)-1] } if input == "" { return Separator() } if volumeName == input { return input } // Throw away volume name input = input[len(filepath.VolumeName(input)):] // Find the last element i := len(input) - 1 for i >= 0 && !IsSeparator(input[i]) { i-- } if i >= 0 { input = input[i+1:] } // If empty now, it had only slashes. if input == "" { return Separator() } return input } func Clean(input string) string { if input == "" { return input } cleaned := input separator := Separator() // The prefix can be empty for a relative path. var prefix string if IsSeparator(cleaned[0]) { prefix = separator } if runtime.GOOS == windows { // Normalize (forward) slashes to backslashes on Windows. cleaned = strings.ReplaceAll(cleaned, "/", `\`) // Clean the prefix for a UNC path, if any. if regex.MatchString(`^\\{2}[^\\]+`, cleaned) { cleaned = strings.TrimPrefix(cleaned, `\\.\UNC\`) if cleaned == "" { return cleaned } prefix = `\\` } // Always use an uppercase drive letter on Windows. driveLetter, err := regex.GetCompiledRegex(`^[a-z]:`) if err == nil { cleaned = driveLetter.ReplaceAllStringFunc(cleaned, strings.ToUpper) } } sb := text.NewBuilder() sb.WriteString(prefix) // Clean slashes. matches := regex.FindAllNamedRegexMatch(fmt.Sprintf(`(?P[^\%s]+)`, separator), cleaned) n := len(matches) - 1 for i, m := range matches { sb.WriteString(m["element"]) if i != n { sb.WriteString(separator) } } return sb.String() } func ReplaceHomeDirPrefixWithTilde(path string) string { home := Home() if !strings.HasPrefix(path, home) { return path } rem := path[len(home):] if rem == "" || IsSeparator(rem[0]) { return "~" + rem } return path } func ReplaceTildePrefixWithHomeDir(path string) string { if !strings.HasPrefix(path, "~") { return path } rem := path[1:] if rem == "" || IsSeparator(rem[0]) { return Home() + rem } return path } ================================================ FILE: src/runtime/path/home.go ================================================ package path import ( "os" "github.com/jandedobbeleer/oh-my-posh/src/log" ) func Home() string { home := os.Getenv("HOME") defer func() { log.Debug(home) }() if len(home) > 0 { return home } // fallback to older implementations on Windows home = os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") if home == "" { home = os.Getenv("USERPROFILE") } return home } ================================================ FILE: src/runtime/path/separator.go ================================================ package path import ( "runtime" "time" "github.com/jandedobbeleer/oh-my-posh/src/log" ) const ( windows = "windows" ) func Separator() string { defer log.Trace(time.Now()) if runtime.GOOS == windows { return `\` } return "/" } func IsSeparator(c uint8) bool { if c == '/' { return true } if runtime.GOOS == windows && c == '\\' { return true } return false } ================================================ FILE: src/runtime/terminal.go ================================================ package runtime import ( "context" "errors" "fmt" "io" "io/fs" httplib "net/http" "net/http/httputil" "os" "os/exec" "path/filepath" "runtime" "strconv" "strings" "time" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/log" "github.com/jandedobbeleer/oh-my-posh/src/maps" "github.com/jandedobbeleer/oh-my-posh/src/regex" "github.com/jandedobbeleer/oh-my-posh/src/runtime/cmd" "github.com/jandedobbeleer/oh-my-posh/src/runtime/http" "github.com/jandedobbeleer/oh-my-posh/src/runtime/path" disk "github.com/shirou/gopsutil/v4/disk" load "github.com/shirou/gopsutil/v4/load" process "github.com/shirou/gopsutil/v4/process" ) type Terminal struct { CmdFlags *Flags cmdCache *cache.Command lsDirMap *maps.Concurrent[[]fs.DirEntry] cwd string host string networks []*Connection } func (term *Terminal) Init(flags *Flags) { defer log.Trace(time.Now()) term.CmdFlags = flags if term.CmdFlags == nil { term.CmdFlags = &Flags{} } term.lsDirMap = maps.NewConcurrent[[]fs.DirEntry]() term.setPromptCount() term.setPwd() term.cmdCache = &cache.Command{ Commands: maps.NewConcurrent[string](), } } func (term *Terminal) Getenv(key string) string { defer log.Trace(time.Now(), key) val := os.Getenv(key) log.Debug(val) return val } func (term *Terminal) Pwd() string { return term.cwd } func (term *Terminal) setPwd() { defer log.Trace(time.Now()) correctPath := func(pwd string) string { if term.GOOS() != WINDOWS { return pwd } // on Windows, and being case sensitive and not consistent and all, this gives silly issues driveLetter, err := regex.GetCompiledRegex(`^[a-z]:`) if err == nil { return driveLetter.ReplaceAllStringFunc(pwd, strings.ToUpper) } return pwd } if term.CmdFlags != nil && term.CmdFlags.PWD != "" { term.cwd = path.Clean(term.CmdFlags.PWD) log.Debug(term.cwd) return } dir, err := os.Getwd() if err != nil { log.Error(err) return } term.cwd = correctPath(dir) log.Debug(term.cwd) } func (term *Terminal) HasFiles(pattern string) bool { return term.HasFilesInDir(term.Pwd(), pattern) } func (term *Terminal) HasFilesInDir(dir, pattern string) bool { defer log.Trace(time.Now(), pattern) fileSystem := os.DirFS(dir) var dirEntries []fs.DirEntry if files, OK := term.lsDirMap.Get(dir); OK { dirEntries = files } if len(dirEntries) == 0 { var err error dirEntries, err = fs.ReadDir(fileSystem, ".") if err != nil { log.Error(err) log.Debug("false") return false } term.lsDirMap.Set(dir, dirEntries) } pattern = strings.ToLower(pattern) for _, match := range dirEntries { if match.IsDir() { continue } matchFileName, err := filepath.Match(pattern, strings.ToLower(match.Name())) if err != nil { log.Error(err) log.Debug("false") return false } if matchFileName { log.Debug("true") return true } } log.Debug("false") return false } func (term *Terminal) HasFileInParentDirs(pattern string, depth uint) bool { defer log.Trace(time.Now(), pattern, fmt.Sprint(depth)) currentFolder := term.Pwd() for c := 0; c < int(depth); c++ { if term.HasFilesInDir(currentFolder, pattern) { log.Debug("true") return true } if dir := filepath.Dir(currentFolder); dir != currentFolder { currentFolder = dir } else { log.Debug("false") return false } } log.Debug("false") return false } func (term *Terminal) HasFolder(folder string) bool { defer log.Trace(time.Now(), folder) f, err := os.Stat(folder) if err != nil { log.Debug("false") return false } isDir := f.IsDir() log.Debugf("%t", isDir) return isDir } func (term *Terminal) ResolveSymlink(input string) (string, error) { defer log.Trace(time.Now(), input) link, err := filepath.EvalSymlinks(input) if err != nil { log.Error(err) return "", err } log.Debug(link) return link, nil } func (term *Terminal) FileContent(file string) string { defer log.Trace(time.Now(), file) if !filepath.IsAbs(file) { file = filepath.Join(term.Pwd(), file) } content, err := os.ReadFile(file) if err != nil { log.Error(err) return "" } fileContent := string(content) log.Debug(fileContent) return fileContent } func (term *Terminal) LsDir(input string) []fs.DirEntry { defer log.Trace(time.Now(), input) entries, err := os.ReadDir(input) if err != nil { log.Error(err) return nil } log.Debugf("%v", entries) return entries } func (term *Terminal) User() string { defer log.Trace(time.Now()) user := os.Getenv("USER") if user == "" { user = os.Getenv("USERNAME") } log.Debug(user) return user } func (term *Terminal) Host() (string, error) { defer log.Trace(time.Now()) if len(term.host) != 0 { return term.host, nil } hostName, err := os.Hostname() if err != nil { log.Error(err) return "", err } hostName = cleanHostName(hostName) log.Debug(hostName) term.host = hostName return hostName, nil } func (term *Terminal) GOOS() string { defer log.Trace(time.Now()) return runtime.GOOS } func (term *Terminal) Home() string { return path.Home() } func (term *Terminal) RunCommand(command string, args ...string) (string, error) { defer log.Trace(time.Now(), append([]string{command}, args...)...) if cacheCommand, ok := term.cmdCache.Get(command); ok { command = cacheCommand } output, err := cmd.Run(command, args...) if err != nil { log.Error(err) } log.Debug(output) return output, err } func (term *Terminal) RunShellCommand(shell, command string) string { defer log.Trace(time.Now()) if out, err := term.RunCommand(shell, "-c", command); err == nil { return out } return "" } func (term *Terminal) CommandPath(command string) string { defer log.Trace(time.Now(), command) if cmdPath, ok := term.cmdCache.Get(command); ok { log.Debug(cmdPath) return cmdPath } cmdPath, err := exec.LookPath(command) if err == nil { term.cmdCache.Set(command, cmdPath) log.Debug(cmdPath) return cmdPath } log.Error(err) return "" } func (term *Terminal) HasCommand(command string) bool { defer log.Trace(time.Now(), command) if cmdPath := term.CommandPath(command); cmdPath != "" { return true } return false } func (term *Terminal) StatusCodes() (int, string) { defer log.Trace(time.Now()) if term.CmdFlags.Shell != CMD || !term.CmdFlags.NoExitCode { return term.CmdFlags.ErrorCode, term.CmdFlags.PipeStatus } errorCode := term.Getenv("=ExitCode") log.Debug(errorCode) term.CmdFlags.ErrorCode, _ = strconv.Atoi(errorCode) return term.CmdFlags.ErrorCode, term.CmdFlags.PipeStatus } func (term *Terminal) ExecutionTime() float64 { defer log.Trace(time.Now()) if term.CmdFlags.ExecutionTime < 0 { return 0 } return term.CmdFlags.ExecutionTime } func (term *Terminal) Flags() *Flags { defer log.Trace(time.Now()) return term.CmdFlags } func (term *Terminal) Shell() string { defer log.Trace(time.Now()) if len(term.CmdFlags.Shell) != 0 { return term.CmdFlags.Shell } log.Debug("no shell name provided in flags, trying to detect it") pid := os.Getppid() p, _ := process.NewProcess(int32(pid)) name, err := p.Name() if err != nil { log.Error(err) return UNKNOWN } log.Debug("process name: " + name) // Cache the shell value to speed things up. term.CmdFlags.Shell = strings.Trim(strings.TrimSuffix(name, ".exe"), " ") return term.CmdFlags.Shell } func (term *Terminal) unWrapError(err error) error { cause := err for { type nested interface{ Unwrap() error } unwrap, ok := cause.(nested) if !ok { break } cause = unwrap.Unwrap() } return cause } func (term *Terminal) HTTPRequest(targetURL string, body io.Reader, timeout int, requestModifiers ...http.RequestModifier) ([]byte, error) { defer log.Trace(time.Now(), targetURL) ctx, cncl := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(timeout)) defer cncl() request, err := httplib.NewRequestWithContext(ctx, httplib.MethodGet, targetURL, body) if err != nil { return nil, err } for _, modifier := range requestModifiers { modifier(request) } if term.CmdFlags.Debug { dump, _ := httputil.DumpRequestOut(request, true) log.Debug(string(dump)) } response, err := http.HTTPClient.Do(request) if err != nil { log.Error(err) return nil, term.unWrapError(err) } // anything inside the range [200, 299] is considered a success if response.StatusCode < 200 || response.StatusCode >= 300 { err := &http.Error{ StatusCode: response.StatusCode, } log.Error(err) return nil, err } defer response.Body.Close() responseBody, err := io.ReadAll(response.Body) if err != nil { log.Error(err) return nil, err } log.Debug(string(responseBody)) return responseBody, nil } func (term *Terminal) HasParentFilePath(parent string, followSymlinks bool) (*FileInfo, error) { defer log.Trace(time.Now(), parent) pwd := term.Pwd() if followSymlinks { if actual, err := term.ResolveSymlink(pwd); err == nil { pwd = actual } } for { fileSystem := os.DirFS(pwd) info, err := fs.Stat(fileSystem, parent) if err == nil { return &FileInfo{ ParentFolder: pwd, Path: filepath.Join(pwd, parent), IsDir: info.IsDir(), }, nil } if !os.IsNotExist(err) { return nil, err } if dir := filepath.Dir(pwd); dir != pwd { pwd = dir continue } log.Error(err) return nil, errors.New("no match at root level") } } func (term *Terminal) StackCount() int { defer log.Trace(time.Now()) if term.CmdFlags.StackCount < 0 { return 0 } return term.CmdFlags.StackCount } func (term *Terminal) Logs() string { return log.String() } func (term *Terminal) DirMatchesOneOf(dir string, regexes []string) (match bool) { // sometimes the function panics inside golang, we want to silence that error // and assume that there's no match. Not perfect, but better than crashing // for the time being until we figure out what the actual root cause is defer func() { if err := recover(); err != nil { log.Error(errors.New("panic")) match = false } }() match = dirMatchesOneOf(dir, term.Home(), term.GOOS(), regexes) return } func dirMatchesOneOf(dir, home, goos string, regexes []string) bool { if len(regexes) == 0 { return false } if goos == WINDOWS { dir = strings.ReplaceAll(dir, "\\", "/") home = strings.ReplaceAll(home, "\\", "/") } for _, element := range regexes { normalized := strings.ReplaceAll(element, "\\\\", "/") if strings.HasPrefix(normalized, "~") { rem := normalized[1:] if rem == "" || rem[0] == '/' { normalized = home + rem } } pattern := fmt.Sprintf("^%s$", normalized) if goos == WINDOWS || goos == DARWIN { pattern = "(?i)" + pattern } matched := regex.MatchString(pattern, dir) if matched { return true } } return false } func (term *Terminal) setPromptCount() { defer log.Trace(time.Now()) var count int if val, found := cache.Get[int](cache.Session, cache.PROMPTCOUNTCACHE); found { count = val } // Only update the count if we're generating a primary prompt. if term.CmdFlags.Type == PRIMARY { count++ cache.Set(cache.Session, cache.PROMPTCOUNTCACHE, count, cache.ONEDAY) } term.CmdFlags.PromptCount = count } func (term *Terminal) CursorPosition() (row, col int) { if number, err := strconv.Atoi(term.Getenv("POSH_CURSOR_LINE")); err == nil { row = number } if number, err := strconv.Atoi(term.Getenv("POSH_CURSOR_COLUMN")); err != nil { col = number } return } func (term *Terminal) SystemInfo() (*SystemInfo, error) { s := &SystemInfo{} mem, err := term.Memory() if err != nil { return nil, err } s.Memory = *mem loadStat, err := load.Avg() if err == nil { s.Load1 = loadStat.Load1 s.Load5 = loadStat.Load5 s.Load15 = loadStat.Load15 } diskIO, err := disk.IOCounters() if err == nil { s.Disks = diskIO } return s, nil } func cleanHostName(hostName string) string { garbage := []string{ ".lan", ".local", ".localdomain", } for _, g := range garbage { if strings.HasSuffix(hostName, g) { hostName = strings.Replace(hostName, g, "", 1) } } return hostName } ================================================ FILE: src/runtime/terminal_darwin.go ================================================ package runtime import ( "errors" "strconv" "strings" "time" "github.com/jandedobbeleer/oh-my-posh/src/log" "github.com/jandedobbeleer/oh-my-posh/src/regex" "github.com/jandedobbeleer/oh-my-posh/src/runtime/battery" ) func mapMostLogicalState(state string) battery.State { switch state { case "charging": return battery.Charging case "discharging": return battery.Discharging case "AC attached": return battery.NotCharging case "full": return battery.Full case "empty": return battery.Empty case "charged": return battery.Full default: return battery.Unknown } } func (term *Terminal) parseBatteryOutput(output string) (*battery.Info, error) { matches := regex.FindNamedRegexMatch(`(?P[0-9]{1,3})%; (?P[a-zA-Z\s]+);`, output) if len(matches) != 2 { err := errors.New("unable to find battery state based on output") log.Error(err) return nil, err } var percentage int var err error if percentage, err = strconv.Atoi(matches["PERCENTAGE"]); err != nil { log.Error(err) return nil, errors.New("unable to parse battery percentage") } return &battery.Info{ Percentage: percentage, State: mapMostLogicalState(matches["STATE"]), }, nil } func (term *Terminal) BatteryState() (*battery.Info, error) { defer log.Trace(time.Now()) output, err := term.RunCommand("pmset", "-g", "batt") if err != nil { log.Error(err) return nil, err } if !strings.Contains(output, "Battery") { return nil, errors.New("no battery found") } return term.parseBatteryOutput(output) } ================================================ FILE: src/runtime/terminal_test.go ================================================ package runtime import ( "testing" "github.com/stretchr/testify/assert" ) func TestNormalHostName(t *testing.T) { hostName := "hello" assert.Equal(t, hostName, cleanHostName(hostName)) } func TestHostNameWithLocal(t *testing.T) { hostName := "hello.local" assert.Equal(t, "hello", cleanHostName(hostName)) } func TestHostNameWithLan(t *testing.T) { hostName := "hello.lan" cleanHostName := cleanHostName(hostName) assert.Equal(t, "hello", cleanHostName) } func TestDirMatchesOneOf(t *testing.T) { cases := []struct { GOOS string HomeDir string Dir string Pattern string Expected bool }{ {GOOS: LINUX, HomeDir: "/home/bill", Dir: "/home/bill", Pattern: "/home/bill", Expected: true}, {GOOS: LINUX, HomeDir: "/home/bill", Dir: "/home/bill/foo", Pattern: "~/foo", Expected: true}, {GOOS: LINUX, HomeDir: "/home/bill", Dir: "/home/bill/foo", Pattern: "~/Foo", Expected: false}, {GOOS: LINUX, HomeDir: "/home/bill", Dir: "/home/bill/foo", Pattern: "~\\\\foo", Expected: true}, {GOOS: LINUX, HomeDir: "/home/bill", Dir: "/home/bill/foo/bar", Pattern: "~/fo.*", Expected: true}, {GOOS: LINUX, HomeDir: "/home/bill", Dir: "/home/bill/foo", Pattern: "~/fo\\w", Expected: true}, {GOOS: WINDOWS, HomeDir: "C:\\Users\\Bill", Dir: "C:\\Users\\Bill", Pattern: "C:\\\\Users\\\\Bill", Expected: true}, {GOOS: WINDOWS, HomeDir: "C:\\Users\\Bill", Dir: "C:\\Users\\Bill", Pattern: "C:/Users/Bill", Expected: true}, {GOOS: WINDOWS, HomeDir: "C:\\Users\\Bill", Dir: "C:\\Users\\Bill", Pattern: "c:/users/bill", Expected: true}, {GOOS: WINDOWS, HomeDir: "C:\\Users\\Bill", Dir: "C:\\Users\\Bill", Pattern: "~", Expected: true}, {GOOS: WINDOWS, HomeDir: "C:\\Users\\Bill", Dir: "C:\\Users\\Bill\\Foo", Pattern: "~/Foo", Expected: true}, {GOOS: WINDOWS, HomeDir: "C:\\Users\\Bill", Dir: "C:\\Users\\Bill\\Foo", Pattern: "~/foo", Expected: true}, {GOOS: WINDOWS, HomeDir: "C:\\Users\\Bill", Dir: "C:\\Users\\Bill\\Foo\\Bar", Pattern: "~/fo.*", Expected: true}, {GOOS: WINDOWS, HomeDir: "C:\\Users\\Bill", Dir: "C:\\Users\\Bill\\Foo", Pattern: "~/fo\\w", Expected: true}, } for _, tc := range cases { got := dirMatchesOneOf(tc.Dir, tc.HomeDir, tc.GOOS, []string{tc.Pattern}) assert.Equal(t, tc.Expected, got) } } func TestDirMatchesOneOfRegexInverted(t *testing.T) { // detect panic(thrown by MustCompile) defer func() { if err := recover(); err != nil { // display a message explaining omp failed(with the err) assert.Equal(t, "regexp: Compile(`^(?!Projects[\\/]).*$`): error parsing regexp: invalid or unsupported Perl syntax: `(?!`", err) } }() _ = dirMatchesOneOf("Projects/oh-my-posh", "", LINUX, []string{"(?!Projects[\\/]).*"}) } func TestDirMatchesOneOfRegexInvertedNonEscaped(t *testing.T) { // detect panic(thrown by MustCompile) defer func() { if err := recover(); err != nil { // display a message explaining omp failed(with the err) assert.Equal(t, "regexp: Compile(`^(?!Projects/).*$`): error parsing regexp: invalid or unsupported Perl syntax: `(?!`", err) } }() _ = dirMatchesOneOf("Projects/oh-my-posh", "", LINUX, []string{"(?!Projects/).*"}) } ================================================ FILE: src/runtime/terminal_unix.go ================================================ //go:build !windows package runtime import ( "os" "strconv" "strings" "time" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/log" "github.com/shirou/gopsutil/v4/host" mem "github.com/shirou/gopsutil/v4/mem" terminal "github.com/wayneashleyberry/terminal-dimensions" "golang.org/x/sys/unix" ) func (term *Terminal) Root() bool { defer log.Trace(time.Now()) return os.Geteuid() == 0 } func (term *Terminal) QueryWindowTitles(_, _ string) (string, error) { return "", &NotImplemented{} } func (term *Terminal) IsWsl() bool { defer log.Trace(time.Now()) const key = "is_wsl" if val, found := cache.Get[bool](cache.Device, key); found { return val } var val bool defer func() { cache.Set(cache.Device, key, val, cache.INFINITE) }() val = term.HasCommand("wslpath") return val } func (term *Terminal) IsWsl2() bool { defer log.Trace(time.Now()) if !term.IsWsl() { return false } uname := term.FileContent("/proc/sys/kernel/osrelease") return strings.Contains(uname, "WSL2") } func (term *Terminal) IsCygwin() bool { defer log.Trace(time.Now()) return false } func (term *Terminal) TerminalWidth() (int, error) { defer log.Trace(time.Now()) if term.CmdFlags.TerminalWidth > 0 { log.Debugf("terminal width: %d", term.CmdFlags.TerminalWidth) return term.CmdFlags.TerminalWidth, nil } width, err := terminal.Width() if err != nil { log.Error(err) } // fetch width from the environment variable // in case the terminal width is not available if width == 0 { i, err := strconv.Atoi(term.Getenv("COLUMNS")) if err != nil { log.Error(err) } width = uint(i) } term.CmdFlags.TerminalWidth = int(width) log.Debugf("terminal width: %d", term.CmdFlags.TerminalWidth) // Claude CLI has a 2 character padding on both sides if term.CmdFlags.Shell == "claude" { log.Debug("adjusting terminal width for Claude CLI") term.CmdFlags.TerminalWidth -= 4 } return term.CmdFlags.TerminalWidth, err } func (term *Terminal) Platform() string { const key = "environment_platform" if val, found := cache.Get[string](cache.Device, key); found { return val } var platform string defer func() { cache.Set(cache.Device, key, platform, cache.INFINITE) }() if wsl := term.Getenv("WSL_DISTRO_NAME"); len(wsl) != 0 { platform, _, _ = strings.Cut(wsl, "-") platform = strings.ToLower(platform) log.Debug(platform) return platform } platform, _, _, _ = host.PlatformInformation() platform = term.getSpecialLinuxDistros(platform) log.Debug(platform) return platform } func (term *Terminal) getSpecialLinuxDistros(platform string) string { lsbInfo := term.FileContent("/etc/lsb-release") if platform == "arch" && strings.Contains(strings.ToLower(lsbInfo), "manjaro") { // validate for Manjaro return "manjaro" } if platform == "debian" && strings.Contains(strings.ToLower(lsbInfo), "zorin") { // validate for Zorin OS return "zorin" } return platform } func (term *Terminal) WindowsRegistryKeyValue(_ string) (*WindowsRegistryValue, error) { return nil, &NotImplemented{} } func (term *Terminal) InWSLSharedDrive() bool { if !term.IsWsl2() { return false } windowsPath := term.ConvertToWindowsPath(term.Pwd()) return !strings.HasPrefix(windowsPath, `//wsl.localhost/`) && !strings.HasPrefix(windowsPath, `//wsl$/`) } func (term *Terminal) ConvertToWindowsPath(input string) string { windowsPath, err := term.RunCommand("wslpath", "-m", input) if err == nil { return windowsPath } return input } func (term *Terminal) ConvertToLinuxPath(input string) string { if linuxPath, err := term.RunCommand("wslpath", "-u", input); err == nil { return linuxPath } return input } func (term *Terminal) DirIsWritable(input string) bool { defer log.Trace(time.Now(), input) return unix.Access(input, unix.W_OK) == nil } func (term *Terminal) Connection(_ ConnectionType) (*Connection, error) { // added to disable the linting error, we can implement this later if len(term.networks) == 0 { return nil, &NotImplemented{} } return nil, &NotImplemented{} } func (term *Terminal) Memory() (*Memory, error) { m := &Memory{} memStat, err := mem.VirtualMemory() if err != nil { log.Error(err) return nil, err } m.PhysicalTotalMemory = memStat.Total m.PhysicalAvailableMemory = memStat.Available m.PhysicalFreeMemory = memStat.Free if memStat.Total > 0 { used := float64(memStat.Total) - float64(memStat.Available) if used < 0 { used = 0 } m.PhysicalPercentUsed = used / float64(memStat.Total) * 100 } swapStat, err := mem.SwapMemory() if err != nil { log.Error(err) } m.SwapTotalMemory = swapStat.Total m.SwapFreeMemory = swapStat.Free m.SwapPercentUsed = swapStat.UsedPercent return m, nil } ================================================ FILE: src/runtime/terminal_unix_test.go ================================================ //go:build !windows package runtime import ( "testing" "github.com/stretchr/testify/assert" ) func TestMemoryPercentageCalculation(t *testing.T) { cases := []struct { Name string Total uint64 Available uint64 ExpectedPercent float64 }{ { Name: "50% usage", Total: 8 * 1024 * 1024 * 1024, Available: 4 * 1024 * 1024 * 1024, ExpectedPercent: 50.0, }, { Name: "37% usage (from issue)", Total: 8079691776, Available: 5093384192, ExpectedPercent: 36.96, }, { Name: "25% usage", Total: 16 * 1024 * 1024 * 1024, Available: 12 * 1024 * 1024 * 1024, ExpectedPercent: 25.0, }, { Name: "75% usage", Total: 8 * 1024 * 1024 * 1024, Available: 2 * 1024 * 1024 * 1024, ExpectedPercent: 75.0, }, { Name: "0% usage", Total: 8 * 1024 * 1024 * 1024, Available: 8 * 1024 * 1024 * 1024, ExpectedPercent: 0.0, }, { Name: "100% usage", Total: 8 * 1024 * 1024 * 1024, Available: 0, ExpectedPercent: 100.0, }, } for _, tc := range cases { t.Run(tc.Name, func(t *testing.T) { var percentUsed float64 if tc.Total > 0 { percentUsed = float64(tc.Total-tc.Available) / float64(tc.Total) * 100 } assert.InDelta(t, tc.ExpectedPercent, percentUsed, 0.01, tc.Name) }) } } ================================================ FILE: src/runtime/terminal_windows.go ================================================ package runtime import ( "errors" "fmt" "strings" "syscall" "time" "github.com/Azure/go-ansiterm/winterm" "github.com/jandedobbeleer/oh-my-posh/src/log" "github.com/jandedobbeleer/oh-my-posh/src/runtime/path" "golang.org/x/sys/windows" "golang.org/x/sys/windows/registry" ) func (term *Terminal) Root() bool { defer log.Trace(time.Now()) var sid *windows.SID // Although this looks scary, it is directly copied from the // official windows documentation. The Go API for this is a // direct wrap around the official C++ API. // See https://docs.microsoft.com/en-us/windows/desktop/api/securitybaseapi/nf-securitybaseapi-checktokenmembership err := windows.AllocateAndInitializeSid( &windows.SECURITY_NT_AUTHORITY, 2, windows.SECURITY_BUILTIN_DOMAIN_RID, windows.DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, &sid) if err != nil { log.Error(err) return false } defer func() { _ = windows.FreeSid(sid) }() // This appears to cast a null pointer so I'm not sure why this // works, but this guy says it does and it Works for Me™: // https://github.com/golang/go/issues/28804#issuecomment-438838144 token := windows.Token(0) member, err := token.IsMember(sid) if err != nil { log.Error(err) return false } return member } func (term *Terminal) QueryWindowTitles(processName, windowTitleRegex string) (string, error) { defer log.Trace(time.Now(), windowTitleRegex) title, err := queryWindowTitles(processName, windowTitleRegex) if err != nil { log.Error(err) } return title, err } func (term *Terminal) IsWsl() bool { defer log.Trace(time.Now()) return false } func (term *Terminal) IsWsl2() bool { defer log.Trace(time.Now()) return false } func (term *Terminal) IsCygwin() bool { defer log.Trace(time.Now()) return len(term.Getenv("OSTYPE")) != 0 || term.Getenv("BROWSER") == "cygstart" } func (term *Terminal) TerminalWidth() (int, error) { defer log.Trace(time.Now()) if term.CmdFlags.TerminalWidth > 0 { log.Debugf("terminal width: %d", term.CmdFlags.TerminalWidth) return term.CmdFlags.TerminalWidth, nil } handle, err := syscall.Open("CONOUT$", syscall.O_RDWR, 0) if err != nil { log.Error(err) return 0, err } info, err := winterm.GetConsoleScreenBufferInfo(uintptr(handle)) if err != nil { log.Error(err) return 0, err } term.CmdFlags.TerminalWidth = int(info.Size.X) log.Debugf("terminal width: %d", term.CmdFlags.TerminalWidth) // Claude CLI has a 2 character padding on both sides if term.CmdFlags.Shell == "claude" { log.Debug("adjusting terminal width for Claude CLI") term.CmdFlags.TerminalWidth -= 4 } return term.CmdFlags.TerminalWidth, nil } func (term *Terminal) Platform() string { return WINDOWS } // Takes a registry path to a key like // // "HKLM\Software\Microsoft\Windows NT\CurrentVersion\EditionID" // // The last part of the path is the key to retrieve. // // If the path ends in "\", the "(Default)" key in that path is retrieved. // // Returns a variant type if successful; nil and an error if not. func (term *Terminal) WindowsRegistryKeyValue(input string) (*WindowsRegistryValue, error) { defer log.Trace(time.Now(), input) // Format: // "HKLM\Software\Microsoft\Windows NT\CurrentVersion\EditionID" // 1 | 2 | 3 // // Split into: // // 1. Root key - extract the root HKEY string and turn this into a handle to get started // 2. Path - open this path // 3. Key - get this key value // // If 3 is "" (i.e. the path ends with "\"), then get (Default) key. // rootKey, regPath, found := strings.Cut(input, `\`) if !found { err := fmt.Errorf("Error, malformed registry path: '%s'", input) log.Error(err) return nil, err } var regKey string if !strings.HasSuffix(regPath, `\`) { regKey = path.Base(regPath) if len(regKey) != 0 { regPath = strings.TrimSuffix(regPath, `\`+regKey) } } var key registry.Key switch rootKey { case "HKCR", "HKEY_CLASSES_ROOT": key = windows.HKEY_CLASSES_ROOT case "HKCC", "HKEY_CURRENT_CONFIG": key = windows.HKEY_CURRENT_CONFIG case "HKCU", "HKEY_CURRENT_USER": key = windows.HKEY_CURRENT_USER case "HKLM", "HKEY_LOCAL_MACHINE": key = windows.HKEY_LOCAL_MACHINE case "HKU", "HKEY_USERS": key = windows.HKEY_USERS default: err := fmt.Errorf("Error, unknown registry key: '%s", rootKey) log.Error(err) return nil, err } k, err := registry.OpenKey(key, regPath, registry.READ) if err != nil { log.Error(err) return nil, err } _, valType, err := k.GetValue(regKey, nil) if err != nil { log.Error(err) return nil, err } var regValue *WindowsRegistryValue switch valType { case windows.REG_SZ, windows.REG_EXPAND_SZ: value, _, _ := k.GetStringValue(regKey) regValue = &WindowsRegistryValue{ValueType: STRING, String: value} case windows.REG_DWORD: value, _, _ := k.GetIntegerValue(regKey) regValue = &WindowsRegistryValue{ValueType: DWORD, DWord: value, String: fmt.Sprintf("0x%08X", value)} case windows.REG_QWORD: value, _, _ := k.GetIntegerValue(regKey) regValue = &WindowsRegistryValue{ValueType: QWORD, QWord: value, String: fmt.Sprintf("0x%016X", value)} case windows.REG_BINARY: value, _, _ := k.GetBinaryValue(regKey) regValue = &WindowsRegistryValue{ValueType: BINARY, String: string(value)} } if regValue == nil { errorLogMsg := fmt.Sprintf("Error, no formatter for type: %d", valType) return nil, errors.New(errorLogMsg) } log.Debug(fmt.Sprintf("%s(%s): %s", regKey, regValue.ValueType, regValue.String)) return regValue, nil } func (term *Terminal) InWSLSharedDrive() bool { return false } func (term *Terminal) ConvertToWindowsPath(input string) string { return strings.ReplaceAll(input, `\`, "/") } func (term *Terminal) ConvertToLinuxPath(input string) string { return input } func (term *Terminal) DirIsWritable(input string) bool { defer log.Trace(time.Now()) return term.isWriteable(input) } func (term *Terminal) Connection(connectionType ConnectionType) (*Connection, error) { if term.networks == nil { networks := term.getConnections() if len(networks) == 0 { return nil, errors.New("no connections found") } term.networks = networks } for _, network := range term.networks { if network.Type == connectionType { return network, nil } } log.Error(fmt.Errorf("network type '%s' not found", connectionType)) return nil, &NotImplemented{} } ================================================ FILE: src/runtime/terminal_windows_nix.go ================================================ //go:build !darwin package runtime import ( "time" "github.com/jandedobbeleer/oh-my-posh/src/log" "github.com/jandedobbeleer/oh-my-posh/src/runtime/battery" ) func (term *Terminal) BatteryState() (*battery.Info, error) { defer log.Trace(time.Now()) info, err := battery.Get() if err != nil { log.Error(err) return nil, err } return info, nil } ================================================ FILE: src/runtime/win32_windows.go ================================================ package runtime import ( "errors" "fmt" "reflect" "strings" "syscall" "unsafe" "github.com/jandedobbeleer/oh-my-posh/src/log" "github.com/jandedobbeleer/oh-my-posh/src/regex" "golang.org/x/sys/windows" ) // win32 specific code // win32 dll load and function definitions var ( user32 = syscall.NewLazyDLL("user32.dll") procEnumWindows = user32.NewProc("EnumWindows") procGetWindowTextW = user32.NewProc("GetWindowTextW") procGetWindowThreadProcessID = user32.NewProc("GetWindowThreadProcessId") psapi = syscall.NewLazyDLL("psapi.dll") getModuleBaseNameA = psapi.NewProc("GetModuleBaseNameA") iphlpapi = syscall.NewLazyDLL("iphlpapi.dll") hGetIfTable2 = iphlpapi.NewProc("GetIfTable2") ) // enumWindows call enumWindows from user32 and returns all active windows // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-enumwindows func enumWindows(enumFunc, lparam uintptr) (err error) { r1, _, e1 := syscall.SyscallN(procEnumWindows.Addr(), enumFunc, lparam, 0) if r1 == 0 { if e1 != 0 { err = error(e1) } else { err = syscall.EINVAL } } return } // getWindowText returns the title and text of a window from a window handle // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowtextw func getWindowText(hwnd syscall.Handle, str *uint16, maxCount int32) (length int32, err error) { r0, _, e1 := syscall.SyscallN(procGetWindowTextW.Addr(), uintptr(hwnd), uintptr(unsafe.Pointer(str)), uintptr(maxCount)) length = int32(r0) if length == 0 { if e1 != 0 { err = error(e1) } else { err = syscall.EINVAL } } return } func getWindowFileName(handle syscall.Handle) (string, error) { var pid int _, _, _ = procGetWindowThreadProcessID.Call(uintptr(handle), uintptr(unsafe.Pointer(&pid))) const query = windows.PROCESS_QUERY_INFORMATION | windows.PROCESS_VM_READ h, err := windows.OpenProcess(query, false, uint32(pid)) if err != nil { return "", errors.New("unable to open window process") } buf := [1024]byte{} length, _, _ := getModuleBaseNameA.Call(uintptr(h), 0, uintptr(unsafe.Pointer(&buf)), 1024) filename := string(buf[:length]) return strings.ToLower(filename), nil } // GetWindowTitle searches for a window attached to the pid func queryWindowTitles(processName, windowTitleRegex string) (string, error) { var title string // callback for EnumWindows cb := syscall.NewCallback(func(handle syscall.Handle, _ uintptr) uintptr { fileName, err := getWindowFileName(handle) if err != nil { // ignore the error and continue enumeration return 1 } if processName != fileName { // ignore the error and continue enumeration return 1 } b := make([]uint16, 200) _, err = getWindowText(handle, &b[0], int32(len(b))) if err != nil { // ignore the error and continue enumeration return 1 } title = syscall.UTF16ToString(b) if regex.MatchString(windowTitleRegex, title) { // will cause EnumWindows to return 0 (error) // but we don't want to enumerate all windows since we got what we want return 0 } return 1 // continue enumeration }) // Enumerates all top-level windows on the screen // The error is not checked because if EnumWindows is stopped before enumerating all windows // it returns 0(error occurred) instead of 1(success) // In our case, title will equal "" or the title of the window anyway err := enumWindows(cb, 0) if title == "" { var message string if err != nil { message = err.Error() } return "", errors.New("no matching window title found\n" + message) } return title, nil } var ( advapi = syscall.NewLazyDLL("advapi32.dll") procGetAce = advapi.NewProc("GetAce") ) const ( ACCESS_DENIED_ACE_TYPE = 1 ) type accessMask uint32 func (m accessMask) canWrite() bool { allowed := []int{windows.GENERIC_WRITE, windows.WRITE_DAC, windows.WRITE_OWNER} for _, v := range allowed { if m&accessMask(v) != 0 { return true } } return false } func (m accessMask) permissions() string { var permissions []string if m&windows.GENERIC_READ != 0 { permissions = append(permissions, "GENERIC_READ") } if m&windows.GENERIC_WRITE != 0 { permissions = append(permissions, "GENERIC_WRITE") } if m&windows.GENERIC_EXECUTE != 0 { permissions = append(permissions, "GENERIC_EXECUTE") } if m&windows.GENERIC_ALL != 0 { permissions = append(permissions, "GENERIC_ALL") } if m&windows.WRITE_DAC != 0 { permissions = append(permissions, "WRITE_DAC") } if m&windows.WRITE_OWNER != 0 { permissions = append(permissions, "WRITE_OWNER") } if m&windows.SYNCHRONIZE != 0 { permissions = append(permissions, "SYNCHRONIZE") } if m&windows.DELETE != 0 { permissions = append(permissions, "DELETE") } if m&windows.READ_CONTROL != 0 { permissions = append(permissions, "READ_CONTROL") } if m&windows.ACCESS_SYSTEM_SECURITY != 0 { permissions = append(permissions, "ACCESS_SYSTEM_SECURITY") } if m&windows.MAXIMUM_ALLOWED != 0 { permissions = append(permissions, "MAXIMUM_ALLOWED") } return strings.Join(permissions, "\n") } type AccessAllowedAce struct { AceType uint8 AceFlags uint8 AceSize uint16 AccessMask accessMask SidStart uint32 } func getCurrentUser() (user *tokenUser, err error) { token := windows.GetCurrentProcessToken() defer token.Close() tokenuser, err := token.GetTokenUser() if err != nil { return } tokenGroups, err := token.GetTokenGroups() if err != nil { return } user = &tokenUser{ sid: tokenuser.User.Sid, groups: tokenGroups.AllGroups(), } return } type tokenUser struct { sid *windows.SID groups []windows.SIDAndAttributes } func (u *tokenUser) isMemberOf(sid *windows.SID) bool { if u.sid.Equals(sid) { return true } for _, g := range u.groups { if g.Sid.Equals(sid) { return true } } return false } func (env *Terminal) isWriteable(folder string) bool { cu, err := getCurrentUser() if err != nil { // unable to get current user log.Error(err) return false } si, err := windows.GetNamedSecurityInfo(folder, windows.SE_FILE_OBJECT, windows.DACL_SECURITY_INFORMATION) if err != nil { log.Error(err) return false } dacl, _, err := si.DACL() if err != nil || dacl == nil { // no dacl implies full access log.Debug("no dacl") return true } rs := reflect.ValueOf(dacl).Elem() aceCount := rs.Field(3).Uint() for i := range aceCount { ace := &AccessAllowedAce{} ret, _, _ := procGetAce.Call(uintptr(unsafe.Pointer(dacl)), uintptr(i), uintptr(unsafe.Pointer(&ace))) if ret == 0 { log.Debug("no ace found") return false } aceSid := (*windows.SID)(unsafe.Pointer(&ace.SidStart)) if !cu.isMemberOf(aceSid) { log.Debug("not current user or in group") continue } log.Debug(fmt.Sprintf("current user is member of %s", aceSid.String())) // this gets priority over the other access types if ace.AceType == ACCESS_DENIED_ACE_TYPE { log.Debug("ACCESS_DENIED_ACE_TYPE") return false } log.Debugf("%v", ace.AccessMask.permissions()) if ace.AccessMask.canWrite() { log.Debug("user has write access") return true } } log.Debug("no write access") return false } var ( kernel32 = syscall.NewLazyDLL("kernel32.dll") globalMemoryStatusEx = kernel32.NewProc("GlobalMemoryStatusEx") ) type memoryStatusEx struct { Length uint32 MemoryLoad uint32 TotalPhys uint64 AvailPhys uint64 TotalPageFile uint64 AvailPageFile uint64 TotalVirtual uint64 AvailVirtual uint64 AvailExtendedVirtual uint64 } func (env *Terminal) Memory() (*Memory, error) { var memStat memoryStatusEx memStat.Length = uint32(unsafe.Sizeof(memStat)) r0, _, err := globalMemoryStatusEx.Call(uintptr(unsafe.Pointer(&memStat))) if r0 == 0 { log.Error(err) return nil, err } return &Memory{ PhysicalTotalMemory: memStat.TotalPhys, PhysicalFreeMemory: memStat.AvailPhys, PhysicalAvailableMemory: memStat.AvailPhys, PhysicalPercentUsed: float64(memStat.MemoryLoad), }, nil } ================================================ FILE: src/segments/angular.go ================================================ package segments import ( "path/filepath" ) type Angular struct { Language } func (a *Angular) Template() string { return languageTemplate } func (a *Angular) Enabled() bool { a.extensions = []string{"angular.json"} a.tooling = map[string]*cmd{ "angular": { regex: `(?:(?P((?P[0-9]+).(?P[0-9]+).(?P[0-9]+))))`, getVersion: a.getVersion, }, } a.defaultTooling = []string{"angular"} a.versionURLTemplate = "https://github.com/angular/angular/releases/tag/{{.Full}}" return a.Language.Enabled() } func (a *Angular) getVersion() (string, error) { return a.nodePackageVersion(filepath.Join("@angular", "core")) } ================================================ FILE: src/segments/argocd.go ================================================ package segments import ( "errors" "os" "path" "strings" "github.com/jandedobbeleer/oh-my-posh/src/log" "github.com/spf13/pflag" yaml "go.yaml.in/yaml/v3" ) const ( argocdOptsEnv = "ARGOCD_OPTS" argocdInvalidYaml = "invalid yaml" argocdNoCurrent = "no current context" NameTemplate = " {{ .Name }} " ) type ArgocdContext struct { Name string `yaml:"name"` Server string `yaml:"server"` User string `yaml:"user"` } type ArgocdConfig struct { CurrentContext string `yaml:"current-context"` Contexts []*ArgocdContext `yaml:"contexts"` } type Argocd struct { Base ArgocdContext } func (a *Argocd) Template() string { return NameTemplate } func (a *Argocd) Enabled() bool { // always parse config instead of using cli to save time configPath := a.getConfigPath() succeeded, err := a.parseConfig(configPath) if err != nil { log.Error(err) return false } return succeeded } func (a *Argocd) getConfigPath() string { cp := path.Join(a.env.Home(), ".config", "argocd", "config") cpo := a.getConfigFromOpts() if len(cpo) > 0 { cp = cpo } return cp } func (a *Argocd) getConfigFromOpts() string { // don't exit/panic when encountering invalid flags flags := pflag.NewFlagSet(os.Args[0], pflag.ContinueOnError) // ignore other valid and invalid flags flags.ParseErrorsAllowlist.UnknownFlags = true // only care about config flags.String("config", "", "get config from opts") opts := a.env.Getenv(argocdOptsEnv) _ = flags.Parse(strings.Split(opts, " ")) return flags.Lookup("config").Value.String() } func (a *Argocd) parseConfig(file string) (bool, error) { config := a.env.FileContent(file) // missing or empty file content if config == "" { return false, errors.New(argocdInvalidYaml) } var data ArgocdConfig err := yaml.Unmarshal([]byte(config), &data) if err != nil { log.Error(err) return false, errors.New(argocdInvalidYaml) } a.Name = data.CurrentContext for _, context := range data.Contexts { if context.Name == a.Name { // mandatory fields in yaml if context.Server == "" || context.User == "" { return false, errors.New(argocdInvalidYaml) } a.Server = context.Server a.User = context.User return true, nil } } return false, errors.New(argocdNoCurrent) } ================================================ FILE: src/segments/argocd_test.go ================================================ package segments import ( "fmt" "path" "testing" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/runtime/mock" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" "github.com/stretchr/testify/assert" ) const ( poshHome = "/Users/posh" ) func TestArgocdGetConfigFromOpts(t *testing.T) { configFile := "/Users/posh/.config/argocd/config" cases := []struct { Case string Opts string Expected string }{ {Case: "invalid flag in opts", Opts: "--invalid", Expected: ""}, {Case: "no config in opts", Opts: "--grpc-web", Expected: ""}, { Case: "config in opts", Opts: fmt.Sprintf("--grpc-web --config %s --plaintext", configFile), Expected: configFile, }, } for _, tc := range cases { env := new(mock.Environment) env.On("Getenv", argocdOptsEnv).Return(tc.Opts) argocd := &Argocd{ Base: Base{ env: env, options: options.Map{}, }, } config := argocd.getConfigFromOpts() assert.Equal(t, tc.Expected, config, tc.Case) } } func TestArgocdGetConfigPath(t *testing.T) { configFile := path.Join(poshHome, ".config", "argocd", "config") cases := []struct { Case string Opts string Expected string ExpectedError string }{ {Case: "without opts", Expected: configFile}, {Case: "with opts", Opts: "--config /etc/argocd/config", Expected: "/etc/argocd/config"}, } for _, tc := range cases { env := new(mock.Environment) env.On("Home").Return(poshHome) env.On("Getenv", argocdOptsEnv).Return(tc.Opts) argocd := &Argocd{ Base: Base{ env: env, options: options.Map{}, }, } assert.Equal(t, tc.Expected, argocd.getConfigPath()) } } func TestArgocdParseConfig(t *testing.T) { configFile := "/Users/posh/.config/argocd/config" cases := []struct { ExpectedContext ArgocdContext Case string Config string ExpectedError string Expected bool }{ {Case: "missing or empty yaml", Config: "", ExpectedError: argocdInvalidYaml}, { Case: "invalid yaml", ExpectedError: argocdInvalidYaml, Config: ` [context] context `, }, { Case: "invalid config", ExpectedError: argocdInvalidYaml, Config: ` contexts: - name: context1 server: server1 user: user1 - name: context2 server: server2 userr: user2 current-context: context2 servers: - grpc-web: true server: server1 - grpc-web: false server: serve2 `, }, { Case: "no current context found", ExpectedError: argocdNoCurrent, Config: ` contexts: - name: context1 server: server1 user: user1 - name: context2 server: server2 user: user2 `, }, { Case: "current context found", Expected: true, Config: ` contexts: - name: context1 server: server1 user: user1 - name: context2 server: server2 user: user2 current-context: context2 servers: - grpc-web: true server: server1 - grpc-web: false server: serve2 users: - auth-token: authtoken1 name: user1 refresh-token: refreshtoken1 - auth-token: authtoken2 name: user2 refresh-token: refreshtoken2 `, ExpectedContext: ArgocdContext{ Name: "context2", Server: "server2", User: "user2", }, }, } for _, tc := range cases { env := new(mock.Environment) env.On("FileContent", configFile).Return(tc.Config) argocd := &Argocd{ Base: Base{ env: env, options: options.Map{}, }, } if len(tc.ExpectedError) > 0 { _, err := argocd.parseConfig(configFile) assert.EqualError(t, err, tc.ExpectedError, tc.Case) continue } config, err := argocd.parseConfig(configFile) assert.NoErrorf(t, err, tc.Case) assert.Equal(t, tc.Expected, config, tc.Case) assert.Equal(t, tc.ExpectedContext, argocd.ArgocdContext, tc.Case) } } func TestArgocdSegment(t *testing.T) { configFile := path.Join(poshHome, ".config", "argocd", "config") cases := []struct { ExpectedContext ArgocdContext Case string Opts string Config string Template string ExpectedString string ExpectedError string ExpectedEnabled bool }{ { Case: "default template", Opts: "", Config: ` contexts: - name: context1 server: server1 user: user1 - name: context2 server: server2 user: user2 current-context: context2 servers: - grpc-web: true server: server1 - grpc-web: false server: serve2 `, ExpectedString: "context2", ExpectedEnabled: true, ExpectedContext: ArgocdContext{ Name: "context2", Server: "server2", User: "user2", }, }, { Case: "full template", Opts: "", Config: ` contexts: - name: context1 server: server1 user: user1 - name: context2 server: server2 user: user2 current-context: context2 servers: - grpc-web: true server: server1 - grpc-web: false server: serve2 `, Template: "{{ .Name }}:{{ .User}}@{{ .Server }}", ExpectedString: "context2:user2@server2", ExpectedEnabled: true, ExpectedContext: ArgocdContext{ Name: "context2", Server: "server2", User: "user2", }, }, { Case: "broken config", Config: `}`, ExpectedEnabled: false, }, } for _, tc := range cases { env := new(mock.Environment) env.On("Home").Return(poshHome) env.On("Getenv", argocdOptsEnv).Return(tc.Opts) env.On("FileContent", configFile).Return(tc.Config) env.On("Flags").Return(&runtime.Flags{}) argocd := &Argocd{} argocd.Init(options.Map{}, env) assert.Equal(t, tc.ExpectedEnabled, argocd.Enabled(), tc.Case) if !tc.ExpectedEnabled { continue } assert.Equal(t, tc.ExpectedContext, argocd.ArgocdContext, tc.Case) if len(tc.Template) > 0 { assert.Equal(t, tc.ExpectedString, renderTemplate(env, tc.Template, argocd), tc.Case) } else { assert.Equal(t, tc.ExpectedString, renderTemplate(env, argocd.Template(), argocd), tc.Case) } } } ================================================ FILE: src/segments/aurelia.go ================================================ package segments type Aurelia struct { Language } func (a *Aurelia) Template() string { return languageTemplate } func (a *Aurelia) Enabled() bool { a.extensions = []string{"package.json"} a.tooling = map[string]*cmd{ "aurelia": { regex: `(?:(?P((?P[0-9]+).(?P[0-9]+).(?P[0-9]+)(-(?P[a-z]+).(?P[0-9]+))?)))`, getVersion: a.getVersion, }, } a.defaultTooling = []string{"aurelia"} a.versionURLTemplate = "https://github.com/aurelia/aurelia/releases/tag/v{{ .Full }}" if !a.hasNodePackage("aurelia") { return false } return a.Language.Enabled() } func (a *Aurelia) getVersion() (string, error) { return a.nodePackageVersion("aurelia") } ================================================ FILE: src/segments/aws.go ================================================ package segments import ( "fmt" "strings" "github.com/jandedobbeleer/oh-my-posh/src/regex" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" ) type Aws struct { Base Profile string Region string } const ( defaultUser = "default" ) func (a *Aws) Template() string { return " {{ .Profile }}{{ if .Region }}@{{ .Region }}{{ end }} " } func (a *Aws) Enabled() bool { getEnvFirstMatch := func(envs ...string) string { for _, env := range envs { value := a.env.Getenv(env) if len(value) != 0 { return value } } return "" } displayDefaultUser := a.options.Bool(options.DisplayDefault, true) a.Profile = getEnvFirstMatch("AWS_VAULT", "AWS_DEFAULT_PROFILE", "AWS_PROFILE") if !displayDefaultUser && a.Profile == defaultUser { return false } a.Region = getEnvFirstMatch("AWS_REGION", "AWS_DEFAULT_REGION") if len(a.Profile) != 0 && len(a.Region) != 0 { return true } if a.Profile == "" && len(a.Region) != 0 && displayDefaultUser { a.Profile = defaultUser return true } a.getConfigFileInfo() if !displayDefaultUser && a.Profile == defaultUser { return false } return len(a.Profile) != 0 } func (a *Aws) getConfigFileInfo() { configPath := a.env.Getenv("AWS_CONFIG_FILE") if configPath == "" { configPath = fmt.Sprintf("%s/.aws/config", a.env.Home()) } config := a.env.FileContent(configPath) configSection := "[default]" if len(a.Profile) != 0 { configSection = fmt.Sprintf("[profile %s]", a.Profile) } configLines := strings.SplitSeq(config, "\n") var sectionActive bool for line := range configLines { if strings.HasPrefix(line, configSection) { sectionActive = true continue } if sectionActive && strings.HasPrefix(line, "region") { splitted := strings.SplitN(line, "=", 3) if len(splitted) >= 2 { a.Region = strings.TrimSpace(splitted[1]) break } } } if a.Profile == "" && len(a.Region) != 0 { a.Profile = defaultUser } } func (a *Aws) RegionAlias() string { if a.Region == "" { return "" } splitted := strings.Split(a.Region, "-") if len(splitted) < 2 { return a.Region } splitted[1] = regex.ReplaceAllString(`orth|outh|ast|est|entral`, splitted[1], "") return strings.Join(splitted, "") } ================================================ FILE: src/segments/aws_test.go ================================================ package segments import ( "testing" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/runtime/mock" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" "github.com/stretchr/testify/assert" ) func TestAWSSegment(t *testing.T) { cases := []struct { Case string ExpectedString string Profile string DefaultProfile string Vault string Region string DefaultRegion string ConfigFile string Template string ExpectedEnabled bool DisplayDefault bool }{ {Case: "enabled with default user", ExpectedString: "default@eu-west", Region: "eu-west", ExpectedEnabled: true, DisplayDefault: true}, {Case: "disabled with default user", ExpectedString: "default@eu-west", Region: "eu-west", ExpectedEnabled: false, DisplayDefault: false}, {Case: "disabled", ExpectedString: "", ExpectedEnabled: false}, {Case: "enabled with default user", ExpectedString: "default@eu-west", Profile: "default", Region: "eu-west", ExpectedEnabled: true, DisplayDefault: true}, {Case: "enabled with default profile", ExpectedString: "default@eu-west", DefaultProfile: "default", Region: "eu-west", ExpectedEnabled: true, DisplayDefault: true}, {Case: "disabled with default user", ExpectedString: "default", Profile: "default", Region: "eu-west", ExpectedEnabled: false, DisplayDefault: false}, {Case: "enabled no region", ExpectedString: "company", ExpectedEnabled: true, Profile: "company"}, {Case: "enabled with region", ExpectedString: "company@eu-west", ExpectedEnabled: true, Profile: "company", Region: "eu-west", DefaultRegion: "us-west"}, {Case: "enabled with default region", ExpectedString: "company@us-west", ExpectedEnabled: true, Profile: "company", DefaultRegion: "us-west"}, { Case: "template: enabled no region", ExpectedString: "profile: company", ExpectedEnabled: true, Profile: "company", Template: "profile: {{.Profile}}{{if .Region}} in {{.Region}}{{end}}", }, { Case: "template: enabled with region", ExpectedString: "profile: company in eu-west", ExpectedEnabled: true, Profile: "company", Region: "eu-west", Template: "profile: {{.Profile}}{{if .Region}} in {{.Region}}{{end}}", }, { Case: "template: enabled with region alias that has compound cardinal direction", ExpectedString: "profile: company in apne3", ExpectedEnabled: true, Profile: "company", Region: "ap-northeast-3", Template: "profile: {{.Profile}}{{if .Region}} in {{.RegionAlias}}{{end}}", }, {Case: "template: invalid", ExpectedString: "{{ .Burp", ExpectedEnabled: true, Profile: "c", Template: "{{ .Burp"}, } for _, tc := range cases { env := new(mock.Environment) env.On("Getenv", "AWS_VAULT").Return(tc.Vault) env.On("Getenv", "AWS_PROFILE").Return(tc.Profile) env.On("Getenv", "AWS_DEFAULT_PROFILE").Return(tc.DefaultProfile) env.On("Getenv", "AWS_REGION").Return(tc.Region) env.On("Getenv", "AWS_DEFAULT_REGION").Return(tc.DefaultRegion) env.On("Getenv", "AWS_CONFIG_FILE").Return(tc.ConfigFile) env.On("FileContent", "/usr/home/.aws/config").Return("") env.On("Home").Return("/usr/home") props := options.Map{ options.DisplayDefault: tc.DisplayDefault, } env.On("Flags").Return(&runtime.Flags{}) aws := &Aws{} aws.Init(props, env) if tc.Template == "" { tc.Template = aws.Template() } assert.Equal(t, tc.ExpectedEnabled, aws.Enabled(), tc.Case) assert.Equal(t, tc.ExpectedString, renderTemplate(env, tc.Template, aws), tc.Case) } } ================================================ FILE: src/segments/az.go ================================================ package segments import ( "encoding/json" "errors" "path/filepath" "strings" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" ) type Az struct { Base Origin string AzureSubscription } const ( Source options.Option = "source" Pwsh = "pwsh" Cli = "cli" // this deprecated value is used to support the old behavior of first_match FirstMatch = "cli|pwsh" azureEnv = "POSH_AZURE_SUBSCRIPTION" ) type AzureConfig struct { InstallationID string `json:"installationId"` Subscriptions []*AzureSubscription `json:"subscriptions"` } type AzureSubscription struct { User *AzureUser `json:"user"` ID string `json:"id"` Name string `json:"name"` State string `json:"state"` TenantID string `json:"tenantId"` TenantDisplayName string `json:"tenantDisplayName"` EnvironmentName string `json:"environmentName"` HomeTenantID string `json:"homeTenantId"` ManagedByTenants []any `json:"managedByTenants"` IsDefault bool `json:"isDefault"` } type AzureUser struct { Name string `json:"name"` Type string `json:"type"` } type AzurePowerShellSubscription struct { Name string `json:"Name"` Account struct { Type string `json:"Type"` } `json:"Account"` Environment struct { Name string `json:"Name"` } `json:"Environment"` Subscription struct { ID string `json:"Id"` Name string `json:"Name"` State string `json:"State"` ExtendedProperties struct { Account string `json:"Account"` } `json:"ExtendedProperties"` } `json:"Subscription"` Tenant struct { ID string `json:"Id"` Name string `json:"Name"` } `json:"Tenant"` } func (a *Az) Template() string { return NameTemplate } func (a *Az) Enabled() bool { source := a.options.String(Source, FirstMatch) // migrate first_match if source == "first_match" { source = FirstMatch } sources := strings.SplitSeq(source, "|") for source := range sources { switch source { case Pwsh: if OK := a.getModuleSubscription(); OK { return OK } case Cli: if OK := a.getCLISubscription(); OK { return OK } } } return false } func (a *Az) FileContentWithoutBom(file string) string { config := a.env.FileContent(file) const ByteOrderMark = "\ufeff" return strings.TrimLeft(config, ByteOrderMark) } func (a *Az) getCLISubscription() bool { cfg, err := a.findConfig("azureProfile.json") if err != nil { return false } content := a.FileContentWithoutBom(cfg) if content == "" { return false } var config AzureConfig if err := json.Unmarshal([]byte(content), &config); err != nil { return false } for _, subscription := range config.Subscriptions { if subscription.IsDefault { a.AzureSubscription = *subscription a.Origin = "CLI" return true } } return false } func (a *Az) getModuleSubscription() bool { envSubscription := a.env.Getenv(azureEnv) if envSubscription == "" { return false } var config AzurePowerShellSubscription if err := json.Unmarshal([]byte(envSubscription), &config); err != nil { return false } a.IsDefault = true a.EnvironmentName = config.Environment.Name a.TenantID = config.Tenant.ID a.ID = config.Subscription.ID a.Name = config.Subscription.Name a.State = config.Subscription.State a.User = &AzureUser{ Name: config.Subscription.ExtendedProperties.Account, Type: config.Account.Type, } a.TenantDisplayName = config.Tenant.Name a.Origin = "PWSH" return true } func (a *Az) findConfig(fileName string) (string, error) { configDirs := []string{ a.env.Getenv("AZURE_CONFIG_DIR"), filepath.Join(a.env.Home(), ".azure"), filepath.Join(a.env.Home(), ".Azure"), } for _, dir := range configDirs { if len(dir) != 0 && a.env.HasFilesInDir(dir, fileName) { return filepath.Join(dir, fileName), nil } } return "", errors.New("azure config dir not found") } ================================================ FILE: src/segments/az_functions.go ================================================ package segments type AzFunc struct { Language } func (az *AzFunc) Template() string { return languageTemplate } func (az *AzFunc) Enabled() bool { az.extensions = []string{"host.json", "local.settings.json", "function.json"} az.tooling = map[string]*cmd{ "func": { executable: "func", args: []string{"--version"}, regex: `(?P[0-9.]+)`, }, } az.defaultTooling = []string{"func"} return az.Language.Enabled() } ================================================ FILE: src/segments/az_test.go ================================================ package segments import ( "os" "path/filepath" "testing" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/runtime/mock" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" "github.com/jandedobbeleer/oh-my-posh/src/template" "github.com/stretchr/testify/assert" ) func TestAzSegment(t *testing.T) { cases := []struct { Case string ExpectedString string Template string Source string ExpectedEnabled bool HasCLI bool HasPowerShell bool }{ { Case: "no config files found", ExpectedEnabled: false, }, { Case: "Az CLI Profile", ExpectedEnabled: true, ExpectedString: "AzureCliCloud", Template: "{{ .EnvironmentName }}", HasCLI: true, }, { Case: "Az Pwsh Profile", ExpectedEnabled: true, ExpectedString: "AzurePoshCloud", Template: "{{ .EnvironmentName }}", HasPowerShell: true, }, { Case: "Az Pwsh Profile", ExpectedEnabled: true, ExpectedString: "AzurePoshCloud", Template: "{{ .EnvironmentName }}", HasPowerShell: true, }, { Case: "Faulty template", ExpectedEnabled: true, ExpectedString: template.IncorrectTemplate, Template: "{{ .Burp }}", HasPowerShell: true, }, { Case: "PWSH", ExpectedEnabled: true, ExpectedString: "PWSH", Template: "{{ .Origin }}", HasPowerShell: true, }, { Case: "CLI", ExpectedEnabled: true, ExpectedString: "CLI", Template: "{{ .Origin }}", HasCLI: true, }, { Case: "Az CLI Profile only", ExpectedEnabled: true, ExpectedString: "AzureCliCloud", Template: "{{ .EnvironmentName }}", HasCLI: true, Source: Cli, }, { Case: "Az CLI Profile only - disabled", ExpectedEnabled: false, Template: "{{ .EnvironmentName }}", HasCLI: false, Source: Cli, }, { Case: "PowerShell Profile only", ExpectedEnabled: true, ExpectedString: "AzurePoshCloud", Template: "{{ .EnvironmentName }}", HasPowerShell: true, Source: Pwsh, }, { Case: "Az CLI Profile only - disabled", ExpectedEnabled: false, Template: "{{ .EnvironmentName }}", Source: Pwsh, }, { Case: "Az CLI account type", ExpectedEnabled: true, ExpectedString: "user", Template: "{{ .User.Type }}", HasCLI: true, Source: Cli, }, } for _, tc := range cases { env := new(mock.Environment) env.On("Home").Return(poshHome) env.On("Flags").Return(&runtime.Flags{}) var azureProfile, azureRmContext string if tc.HasCLI { content, _ := os.ReadFile("../test/azureProfile.json") azureProfile = string(content) } if tc.HasPowerShell { content, _ := os.ReadFile("../test/AzureRmContext.json") azureRmContext = string(content) } env.On("GOOS").Return(runtime.LINUX) env.On("FileContent", filepath.Join(poshHome, ".azure", "azureProfile.json")).Return(azureProfile) env.On("Getenv", "POSH_AZURE_SUBSCRIPTION").Return(azureRmContext) env.On("Getenv", "AZURE_CONFIG_DIR").Return("") if tc.HasCLI { env.On("HasFilesInDir", filepath.Clean("/Users/posh/.azure"), "azureProfile.json").Return(true) } else { env.On("HasFilesInDir", filepath.Clean("/Users/posh/.azure"), "azureProfile.json").Return(false) env.On("HasFilesInDir", filepath.Clean("/Users/posh/.Azure"), "azureProfile.json").Return(false) } if tc.Source == "" { tc.Source = FirstMatch } az := &Az{} az.Init(options.Map{}, env) assert.Equal(t, tc.ExpectedEnabled, az.Enabled(), tc.Case) assert.Equal(t, tc.ExpectedString, renderTemplate(env, tc.Template, az), tc.Case) } } ================================================ FILE: src/segments/azd.go ================================================ package segments import ( "encoding/json" "path/filepath" "strings" "github.com/jandedobbeleer/oh-my-posh/src/log" ) type Azd struct { Base AzdConfig } type AzdConfig struct { DefaultEnvironment string `json:"defaultEnvironment"` Version int `json:"version"` } func (t *Azd) Template() string { return " \uebd8 {{ .DefaultEnvironment }} " } func (t *Azd) Enabled() bool { var parentFilePath string folders := t.options.StringArray(LanguageFolders, []string{".azure"}) for _, folder := range folders { if file, err := t.env.HasParentFilePath(folder, false); err == nil { parentFilePath = file.ParentFolder break } } if parentFilePath == "" { log.Debug("no .azure folder found in parent directories") return false } dotAzureFolder := filepath.Join(parentFilePath, ".azure") files := t.env.LsDir(dotAzureFolder) for _, file := range files { if file.IsDir() { continue } if strings.EqualFold(file.Name(), "config.json") { return t.TryReadConfigJSON(filepath.Join(dotAzureFolder, file.Name())) } } return false } func (t *Azd) TryReadConfigJSON(file string) bool { if file == "" { return false } content := t.env.FileContent(file) var config AzdConfig if err := json.Unmarshal([]byte(content), &config); err != nil { return false } t.AzdConfig = config return true } ================================================ FILE: src/segments/azd_test.go ================================================ package segments import ( "errors" "io/fs" "path/filepath" "testing" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/runtime/mock" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" "github.com/stretchr/testify/assert" ) func TestAzdSegment(t *testing.T) { cases := []struct { Case string ExpectedString string Template string ExpectedEnabled bool IsInited bool }{ { Case: "no .azure directory found", ExpectedEnabled: false, }, { Case: "Environment located", ExpectedEnabled: true, ExpectedString: "TestEnvironment", Template: "{{ .DefaultEnvironment }}", IsInited: true, }, } for _, tc := range cases { env := new(mock.Environment) env.On("Flags").Return(&runtime.Flags{}) if tc.IsInited { fileInfo := &runtime.FileInfo{ Path: "test/.azure", ParentFolder: "test", IsDir: true, } env.On("HasParentFilePath", ".azure", false).Return(fileInfo, nil) dirEntries := []fs.DirEntry{ &MockDirEntry{ name: "config.json", isDir: false, }, &MockDirEntry{ name: "TestEnvironment", isDir: true, }, } env.On("LsDir", filepath.Join("test", ".azure")).Return(dirEntries, nil) env.On("FileContent", filepath.Join("test", ".azure", "config.json")).Return(`{"version": 1, "defaultEnvironment": "TestEnvironment"}`, nil) } else { env.On("HasParentFilePath", ".azure", false).Return(&runtime.FileInfo{}, errors.New("no such file or directory")) } azd := Azd{} azd.Init(options.Map{}, env) assert.Equal(t, tc.ExpectedEnabled, azd.Enabled(), tc.Case) assert.Equal(t, tc.ExpectedString, renderTemplate(env, tc.Template, azd), tc.Case) } } ================================================ FILE: src/segments/base.go ================================================ package segments import ( "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" ) type Base struct { options options.Provider env runtime.Environment Segment *Segment } type Segment struct { Text string Index int } func (b *Base) Text() string { return b.Segment.Text } func (b *Base) SetText(text string) { b.Segment.Text = text } func (b *Base) SetIndex(index int) { b.Segment.Index = index } func (b *Base) Init(opts options.Provider, env runtime.Environment) { b.Segment = &Segment{} b.options = opts b.env = env } func (b *Base) CacheKey() (string, bool) { return "", false } ================================================ FILE: src/segments/battery.go ================================================ package segments import ( "github.com/jandedobbeleer/oh-my-posh/src/runtime/battery" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" ) type Battery struct { Base Error string Icon string battery.Info } const ( // ChargingIcon to display when charging ChargingIcon options.Option = "charging_icon" // DischargingIcon o display when discharging DischargingIcon options.Option = "discharging_icon" // ChargedIcon to display when fully charged ChargedIcon options.Option = "charged_icon" // NotChargingIcon to display when on AC power NotChargingIcon options.Option = "not_charging_icon" ) func (b *Battery) Template() string { return " {{ if not .Error }}{{ .Icon }}{{ .Percentage }}{{ end }}{{ .Error }} " } func (b *Battery) Enabled() bool { // disable in WSL1 if b.env.IsWsl() && !b.env.IsWsl2() { return false } info, err := b.env.BatteryState() if !b.enabledWhileError(err) { return false } b.Info = *info // case on computer without batteries(no error, empty array) if err == nil && b.Percentage == 0 { return false } switch b.State { case battery.Discharging: b.Icon = b.options.String(DischargingIcon, "") case battery.NotCharging: b.Icon = b.options.String(NotChargingIcon, "") case battery.Charging: b.Icon = b.options.String(ChargingIcon, "") case battery.Full: b.Icon = b.options.String(ChargedIcon, "") case battery.Empty, battery.Unknown: return true } return true } func (b *Battery) enabledWhileError(err error) bool { if err == nil { return true } if _, ok := err.(*battery.NoBatteryError); ok { return false } displayError := b.options.Bool(options.DisplayError, false) if !displayError { return false } b.Error = err.Error() // On Windows, it sometimes errors when the battery is full. // This hack ensures we display a fully charged battery, even if // that state can be incorrect. It's better to "ignore" the error // than to not display the segment at all as that will confuse users. b.Percentage = 100 b.State = battery.Full return true } ================================================ FILE: src/segments/bazel.go ================================================ package segments import "github.com/jandedobbeleer/oh-my-posh/src/segments/options" type Bazel struct { Icon string Language } const ( // Bazel's icon Icon options.Option = "icon" ) func (b *Bazel) Template() string { return " {{ if .Error }}{{ .Icon }} {{ .Error }}{{ else }}{{ url .Icon .URL }} {{ .Full }}{{ end }} " } func (b *Bazel) Enabled() bool { b.extensions = []string{"*.bazel", "*.bzl", "BUILD", "WORKSPACE", ".bazelrc", ".bazelversion"} b.folders = []string{"bazel-bin", "bazel-out", "bazel-testlogs"} b.tooling = map[string]*cmd{ "bazel": { executable: "bazel", args: []string{"--version"}, regex: `bazel (?P((?P[0-9]+).(?P[0-9]+).(?P[0-9]+)))`, }, } b.defaultTooling = []string{"bazel"} // Use the correct URL for Bazel >5.4.1, since they do not have the docs subdomain. b.versionURLTemplate = "https://{{ if lt .Major 6 }}docs.{{ end }}bazel.build/versions/{{ .Major }}.{{ .Minor }}.{{ .Patch }}" b.Icon = b.options.String(Icon, "\ue63a") return b.Language.Enabled() } ================================================ FILE: src/segments/bazel_test.go ================================================ package segments import ( "fmt" "testing" "github.com/stretchr/testify/assert" ) func TestBazel(t *testing.T) { cases := []struct { Case string ExpectedString string Version string Template string }{ {Case: "bazel 4.0.0", ExpectedString: "https://docs.bazel.build/versions/4.0.0\ue63a 4.0.0", Version: "bazel 4.0.0", Template: ""}, {Case: "bazel 5.4.1", ExpectedString: "https://docs.bazel.build/versions/5.4.1\ue63a 5.4.1", Version: "bazel 5.4.1", Template: ""}, {Case: "bazel 6.4.0", ExpectedString: "https://bazel.build/versions/6.4.0\ue63a 6.4.0", Version: "bazel 6.4.0", Template: ""}, {Case: "bazel 7.1.1", ExpectedString: "https://bazel.build/versions/7.1.1\ue63a 7.1.1", Version: "bazel 7.1.1", Template: ""}, {Case: "bazel 10.11.12", ExpectedString: "https://bazel.build/versions/10.11.12\ue63a 10.11.12", Version: "bazel 10.11.12", Template: ""}, {Case: "", ExpectedString: "\ue63a err parsing info from bazel with", Version: "", Template: ""}, } for _, tc := range cases { params := &mockedLanguageParams{ cmd: "bazel", versionParam: "--version", versionOutput: tc.Version, extension: "*.bazel", } env, props := getMockedLanguageEnv(params) props[Icon] = "\ue63a" b := &Bazel{} b.Init(props, env) failMsg := fmt.Sprintf("Failed in case: %s", tc.Case) assert.True(t, b.Enabled(), failMsg) assert.Equal(t, tc.ExpectedString, renderTemplate(env, b.Template(), b), failMsg) } } ================================================ FILE: src/segments/brewfather.go ================================================ package segments import ( "encoding/base64" "encoding/json" "errors" "fmt" "math" "net/http" "sort" "time" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" ) // segment struct, makes templating easier type Brewfather struct { Base DaysBottledOrFermented *uint TemperatureTrendIcon string StatusIcon string DayIcon string URL string Batch ReadingAge int DaysFermenting uint DaysBottled uint } const ( BFUserID options.Option = "user_id" BFBatchID options.Option = "batch_id" BFDoubleUpIcon options.Option = "doubleup_icon" BFSingleUpIcon options.Option = "singleup_icon" BFFortyFiveUpIcon options.Option = "fortyfiveup_icon" BFFlatIcon options.Option = "flat_icon" BFFortyFiveDownIcon options.Option = "fortyfivedown_icon" BFSingleDownIcon options.Option = "singledown_icon" BFDoubleDownIcon options.Option = "doubledown_icon" BFPlanningStatusIcon options.Option = "planning_status_icon" BFBrewingStatusIcon options.Option = "brewing_status_icon" BFFermentingStatusIcon options.Option = "fermenting_status_icon" BFConditioningStatusIcon options.Option = "conditioning_status_icon" BFCompletedStatusIcon options.Option = "completed_status_icon" BFArchivedStatusIcon options.Option = "archived_status_icon" BFDayIcon options.Option = "day_icon" BFStatusPlanning string = "Planning" BFStatusBrewing string = "Brewing" BFStatusFermenting string = "Fermenting" BFStatusConditioning string = "Conditioning" BFStatusCompleted string = "Completed" BFStatusArchived string = "Archived" ) // Returned from https://api.brewfather.app/v1/batches/batch_id/readings type BatchReading struct { Comment string `json:"comment"` DeviceType string `json:"type"` DeviceID string `json:"id"` Gravity float64 `json:"sg"` Temperature float64 `json:"temp"` Timepoint int64 `json:"timepoint"` Time int64 `json:"time"` } type Batch struct { Reading *BatchReading Status string `json:"status"` BatchName string `json:"name"` Recipe struct { Name string `json:"name"` } `json:"recipe"` BatchNumber int `json:"batchNo"` BrewDate int64 `json:"brewDate"` FermentStartDate int64 `json:"fermentationStartDate"` BottlingDate int64 `json:"bottlingDate"` MeasuredOg float64 `json:"measuredOg"` MeasuredFg float64 `json:"measuredFg"` MeasuredAbv float64 `json:"measuredAbv"` TemperatureTrend float64 } func (bf *Brewfather) Template() string { return " {{ .StatusIcon }} {{ if .DaysBottledOrFermented }}{{ .DaysBottledOrFermented }}{{ .DayIcon }} {{ end }}{{ url .Recipe.Name .URL }} {{ printf \"%.1f\" .MeasuredAbv }}%{{ if and (.Reading) (eq .Status \"Fermenting\") }} {{ printf \"%.3f\" .Reading.Gravity }} {{ .Reading.Temperature }}\u00b0 {{ .TemperatureTrendIcon }}{{ end }} " //nolint:lll } func (bf *Brewfather) Enabled() bool { data, err := bf.getResult() if err != nil { return false } bf.Batch = *data if bf.Reading != nil { readingDate := time.UnixMilli(bf.Reading.Time) bf.ReadingAge = int(time.Since(readingDate).Hours()) } else { bf.ReadingAge = -1 } bf.TemperatureTrendIcon = bf.getTrendIcon(bf.TemperatureTrend) bf.StatusIcon = bf.getBatchStatusIcon(data.Status) fermStartDate := time.UnixMilli(bf.FermentStartDate) bottlingDate := time.UnixMilli(bf.BottlingDate) switch bf.Status { case BFStatusFermenting: // in the fermenter now, so relative to today. bf.DaysFermenting = uint(time.Since(fermStartDate).Hours() / 24) bf.DaysBottled = 0 bf.DaysBottledOrFermented = &bf.DaysFermenting case BFStatusConditioning, BFStatusCompleted, BFStatusArchived: bf.DaysFermenting = uint(bottlingDate.Sub(fermStartDate).Hours() / 24) bf.DaysBottled = uint(time.Since(bottlingDate).Hours() / 24) bf.DaysBottledOrFermented = &bf.DaysBottled default: bf.DaysFermenting = 0 bf.DaysBottled = 0 bf.DaysBottledOrFermented = nil } // URL property set to weblink to the full batch page batchID := bf.options.String(BFBatchID, "") if len(batchID) > 0 { bf.URL = fmt.Sprintf("https://web.brewfather.app/tabs/batches/batch/%s", batchID) } bf.DayIcon = bf.options.String(BFDayIcon, "d") return true } func (bf *Brewfather) getTrendIcon(trend float64) string { // Not a fan of this logic - wondering if Go lets us do something cleaner... if trend >= 0 { if trend > 4 { return bf.options.String(BFDoubleUpIcon, "↑↑") } if trend > 2 { return bf.options.String(BFSingleUpIcon, "↑") } if trend > 0.5 { return bf.options.String(BFFortyFiveUpIcon, "↗") } return bf.options.String(BFFlatIcon, "→") } if trend < -4 { return bf.options.String(BFDoubleDownIcon, "↓↓") } if trend < -2 { return bf.options.String(BFSingleDownIcon, "↓") } if trend < -0.5 { return bf.options.String(BFFortyFiveDownIcon, "↘") } return bf.options.String(BFFlatIcon, "→") } func (bf *Brewfather) getBatchStatusIcon(batchStatus string) string { switch batchStatus { case BFStatusPlanning: return bf.options.String(BFPlanningStatusIcon, "\uF8EA") case BFStatusBrewing: return bf.options.String(BFBrewingStatusIcon, "\uF7DE") case BFStatusFermenting: return bf.options.String(BFFermentingStatusIcon, "\uF499") case BFStatusConditioning: return bf.options.String(BFConditioningStatusIcon, "\uE372") case BFStatusCompleted: return bf.options.String(BFCompletedStatusIcon, "\uF7A5") case BFStatusArchived: return bf.options.String(BFArchivedStatusIcon, "\uF187") default: return "" } } func (bf *Brewfather) getResult() (*Batch, error) { userID := bf.options.Template(BFUserID, "", bf) if userID == "" { return nil, errors.New("missing Brewfather user id (user_id)") } apiKey := bf.options.Template(APIKey, "", bf) if apiKey == "" { return nil, errors.New("missing Brewfather api key (api_key)") } batchID := bf.options.Template(BFBatchID, "", bf) if batchID == "" { return nil, errors.New("missing Brewfather batch id (batch_id)") } authString := fmt.Sprintf("%s:%s", userID, apiKey) authStringb64 := base64.StdEncoding.EncodeToString([]byte(authString)) authHeader := fmt.Sprintf("Basic %s", authStringb64) batchURL := fmt.Sprintf("https://api.brewfather.app/v1/batches/%s", batchID) batchReadingsURL := fmt.Sprintf("https://api.brewfather.app/v1/batches/%s/readings", batchID) httpTimeout := bf.options.Int(options.HTTPTimeout, options.DefaultHTTPTimeout) // batch addAuthHeader := func(request *http.Request) { request.Header.Add("authorization", authHeader) } body, err := bf.env.HTTPRequest(batchURL, nil, httpTimeout, addAuthHeader) if err != nil { return nil, err } var batch Batch err = json.Unmarshal(body, &batch) if err != nil { return nil, err } // readings body, err = bf.env.HTTPRequest(batchReadingsURL, nil, httpTimeout, addAuthHeader) if err != nil { return nil, err } var arr []*BatchReading err = json.Unmarshal(body, &arr) if err != nil { return nil, err } if len(arr) > 0 { // could just take latest reading using their API, but that won't allow us to see trend - get 'em all and sort by time, // using two most recent for trend sort.Slice(arr, func(i, j int) bool { return arr[i].Time > arr[j].Time }) // Keep the latest one batch.Reading = arr[0] if len(arr) > 1 { batch.TemperatureTrend = arr[0].Temperature - arr[1].Temperature } } return &batch, nil } // Unit conversion functions available to template. func (bf *Brewfather) DegCToF(degreesC float64) float64 { return math.Round(10*((degreesC*1.8)+32)) / 10 // 1 decimal place } func (bf *Brewfather) DegCToKelvin(degreesC float64) float64 { return math.Round(10*(degreesC+273.15)) / 10 // 1 decimal place, only addition, but just to be sure } func (bf *Brewfather) SGToBrix(sg float64) float64 { // from https://en.wikipedia.org/wiki/Brix#Specific_gravity_2 return math.Round(100*((182.4601*sg*sg*sg)-(775.6821*sg*sg)+(1262.7794*sg)-669.5622)) / 100 } func (bf *Brewfather) SGToPlato(sg float64) float64 { // from https://en.wikipedia.org/wiki/Brix#Specific_gravity_2 return math.Round(100*((135.997*sg*sg*sg)-(630.272*sg*sg)+(1111.14*sg)-616.868)) / 100 // 2 decimal places } ================================================ FILE: src/segments/brewfather_test.go ================================================ package segments import ( "encoding/json" "testing" "time" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/runtime/mock" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" "github.com/stretchr/testify/assert" ) const ( BFFakeBatchID = "FAKE" BFBatchURL = "https://api.brewfather.app/v1/batches/" + BFFakeBatchID BFCacheKey = BFBatchURL BFBatchReadingsURL = "https://api.brewfather.app/v1/batches/" + BFFakeBatchID + "/readings" ) var ( TimeNow = time.Now() // Create a fake timeline for the fake json, all in Unix milliseconds, to be used in all fake json responses FakeBrewDate = TimeNow.Add(-time.Hour * 24 * 20) FakeFermentationStartDate = FakeBrewDate.Add(time.Hour * 24) // 1 day after brew date = 19 days ago FakeReading1Date = FakeFermentationStartDate.Add(time.Minute * 35) // first reading 35 minutes FakeReading2Date = FakeReading1Date.Add(time.Hour) // second reading 1 hour later FakeReading3Date = FakeReading2Date.Add(time.Hour * 3) // 3 hours after last reading, 454 hours ago FakeBottlingDate = FakeFermentationStartDate.Add(time.Hour * 24 * 14) // 14 days after ferm date = 5 days ago BrewDateMillis = FakeBrewDate.UnixMilli() FermStartDateMillis = FakeFermentationStartDate.UnixMilli() Reading1DateMillis = FakeReading1Date.UnixMilli() Reading2DateMillis = FakeReading2Date.UnixMilli() Reading3DateMillis = FakeReading3Date.UnixMilli() BottlingDateMillis = FakeBottlingDate.UnixMilli() BatchNumber = 18 BatchName = "Batch" RecipeName = "Fake Beer" MeasuredAbv = 1.3 ) func createBatch(status string) *Batch { return &Batch{ Status: status, BatchNumber: BatchNumber, BrewDate: BrewDateMillis, FermentStartDate: FermStartDateMillis, BottlingDate: BottlingDateMillis, BatchName: BatchName, MeasuredAbv: MeasuredAbv, Recipe: struct { Name string `json:"name"` }{ Name: RecipeName, }, } } func createReading(temp, gravity float64, millis int64) *BatchReading { return &BatchReading{ DeviceID: "manual", Temperature: temp, Comment: "", Gravity: gravity, Time: millis, DeviceType: "manual", } } func TestBrewfatherSegment(t *testing.T) { cases := []struct { Error error BatchResponse *Batch Case string ExpectedString string Template string BatchReadingsResponse []*BatchReading CacheTimeout int ExpectedEnabled bool CacheFoundFail bool }{ { Case: "Planning Status", BatchResponse: createBatch(BFStatusPlanning), BatchReadingsResponse: []*BatchReading{}, Template: "{{.StatusIcon}} {{if .DaysBottledOrFermented}}{{.DaysBottledOrFermented}}d {{end}}{{.Recipe.Name}} {{.MeasuredAbv}}%{{ if and (.Reading) (eq .Status \"Fermenting\")}}: {{.Reading.Gravity}} {{.Reading.Temperature}}° {{.TemperatureTrendIcon}}{{end}}", //nolint:lll ExpectedString: " Fake Beer 1.3%", ExpectedEnabled: true, }, { Case: "Brewing Status", BatchResponse: createBatch(BFStatusBrewing), BatchReadingsResponse: []*BatchReading{}, Template: "{{.StatusIcon}} {{if .DaysBottledOrFermented}}{{.DaysBottledOrFermented}}d {{end}}{{.Recipe.Name}} {{.MeasuredAbv}}%{{ if and (.Reading) (eq .Status \"Fermenting\")}}: {{.Reading.Gravity}} {{.Reading.Temperature}}° {{.TemperatureTrendIcon}}{{end}}", //nolint:lll ExpectedString: " Fake Beer 1.3%", ExpectedEnabled: true, }, { Case: "Fermenting Status, no readings", BatchResponse: createBatch(BFStatusFermenting), BatchReadingsResponse: []*BatchReading{}, Template: "{{.StatusIcon}} {{if .DaysBottledOrFermented}}{{.DaysBottledOrFermented}}d {{end}}{{.Recipe.Name}} {{.MeasuredAbv}}%{{ if and (.Reading) (eq .Status \"Fermenting\")}}: {{.Reading.Gravity}} {{.Reading.Temperature}}° {{.TemperatureTrendIcon}}{{end}}", //nolint:lll ExpectedString: " 19d Fake Beer 1.3%", ExpectedEnabled: true, }, { Case: "Fermenting Status, one reading", BatchResponse: createBatch(BFStatusFermenting), BatchReadingsResponse: []*BatchReading{ createReading(19.5, 1.066, Reading1DateMillis), }, Template: "{{.StatusIcon}} {{if .DaysBottledOrFermented}}{{.DaysBottledOrFermented}}d {{end}}{{.Recipe.Name}} {{.MeasuredAbv}}%{{ if and (.Reading) (eq .Status \"Fermenting\")}}: {{.Reading.Gravity}} {{.Reading.Temperature}}° {{.TemperatureTrendIcon}}{{end}}", //nolint:lll ExpectedString: " 19d Fake Beer 1.3%: 1.066 19.5° →", ExpectedEnabled: true, }, { Case: "Fermenting Status, two readings, temp trending up", BatchResponse: createBatch(BFStatusFermenting), BatchReadingsResponse: []*BatchReading{ createReading(21.0, 1.063, Reading2DateMillis), createReading(19.5, 1.066, Reading1DateMillis), }, Template: "{{.StatusIcon}} {{if .DaysBottledOrFermented}}{{.DaysBottledOrFermented}}d {{end}}{{.Recipe.Name}} {{.MeasuredAbv}}%{{ if and (.Reading) (eq .Status \"Fermenting\")}}: {{.Reading.Gravity}} {{.Reading.Temperature}}° {{.TemperatureTrendIcon}}{{end}}", //nolint:lll ExpectedString: " 19d Fake Beer 1.3%: 1.063 21° ↗", ExpectedEnabled: true, }, { Case: "Fermenting Status, three readings, temp trending hard down, include age of most recent reading", BatchResponse: createBatch(BFStatusFermenting), BatchReadingsResponse: []*BatchReading{ createReading(15.0, 1.050, Reading3DateMillis), createReading(21.0, 1.063, Reading2DateMillis), createReading(19.5, 1.066, Reading1DateMillis), }, Template: "{{.StatusIcon}} {{.ReadingAge}} {{if .DaysBottledOrFermented}}{{.DaysBottledOrFermented}}d {{end}}{{.Recipe.Name}} {{.MeasuredAbv}}%{{ if and (.Reading) (eq .Status \"Fermenting\")}}: {{.Reading.Gravity}} {{.Reading.Temperature}}° {{.TemperatureTrendIcon}}{{end}}", //nolint:lll ExpectedString: " 451 19d Fake Beer 1.3%: 1.05 15° ↓↓", ExpectedEnabled: true, }, { Case: "Bad batch json, readings fine", BatchResponse: nil, BatchReadingsResponse: []*BatchReading{ createReading(15.0, 1.050, Reading3DateMillis), }, Template: "{{.StatusIcon}} {{.ReadingAge}} {{if .DaysBottledOrFermented}}{{.DaysBottledOrFermented}}d {{end}}{{.Recipe.Name}} {{.MeasuredAbv}}%{{ if and (.Reading) (eq .Status \"Fermenting\")}}: {{.Reading.Gravity}} {{.Reading.Temperature}}° {{.TemperatureTrendIcon}}{{end}}", //nolint:lll ExpectedString: "", ExpectedEnabled: false, }, { Case: "Conditioning Status", BatchResponse: createBatch(BFStatusConditioning), BatchReadingsResponse: []*BatchReading{ createReading(15.0, 1.050, Reading3DateMillis), }, Template: "{{.StatusIcon}} {{if .DaysBottledOrFermented}}{{.DaysBottledOrFermented}}d {{end}}{{.Recipe.Name}} {{.MeasuredAbv}}%{{ if and (.Reading) (eq .Status \"Fermenting\")}}: {{.Reading.Gravity}} {{.Reading.Temperature}}° {{.TemperatureTrendIcon}}{{end}}", //nolint:lll ExpectedString: " 5d Fake Beer 1.3%", ExpectedEnabled: true, }, { Case: "Fermenting Status, test all unit conversions", BatchResponse: createBatch(BFStatusFermenting), BatchReadingsResponse: []*BatchReading{ createReading(34.5, 1.066, Reading1DateMillis), }, Template: "{{ if and (.Reading) (eq .Status \"Fermenting\") }}SG: ({{.Reading.Gravity}} Bx:{{.SGToBrix .Reading.Gravity}} P:{{.SGToPlato .Reading.Gravity}}), Temp: (C:{{.Reading.Temperature}} F:{{.DegCToF .Reading.Temperature}} K:{{.DegCToKelvin .Reading.Temperature}}){{end}}", //nolint:lll ExpectedString: "SG: (1.066 Bx:16.13 P:16.13), Temp: (C:34.5 F:94.1 K:307.7)", ExpectedEnabled: true, }, { Case: "Fermenting Status, test all unit conversions 2", BatchResponse: createBatch(BFStatusFermenting), BatchReadingsResponse: []*BatchReading{ createReading(3.5, 1.004, Reading1DateMillis), }, Template: "{{ if and (.Reading) (eq .Status \"Fermenting\") }}SG: ({{.Reading.Gravity}} Bx:{{.SGToBrix .Reading.Gravity}} P:{{.SGToPlato .Reading.Gravity}}), Temp: (C:{{.Reading.Temperature}} F:{{.DegCToF .Reading.Temperature}} K:{{.DegCToKelvin .Reading.Temperature}}){{end}}", //nolint:lll ExpectedString: "SG: (1.004 Bx:1.03 P:1.03), Temp: (C:3.5 F:38.3 K:276.7)", ExpectedEnabled: true, }, } for _, tc := range cases { env := &mock.Environment{} props := options.Map{ BFBatchID: BFFakeBatchID, APIKey: "FAKE", BFUserID: "FAKE", } var batchJSON []byte var err error if tc.BatchResponse != nil { batchJSON, err = json.Marshal(tc.BatchResponse) assert.NoError(t, err) } else { // bad JSON batchJSON = []byte("invalid json") } batchReadingsJSON, err := json.Marshal(tc.BatchReadingsResponse) assert.NoError(t, err) env.On("HTTPRequest", BFBatchURL).Return(batchJSON, tc.Error) env.On("HTTPRequest", BFBatchReadingsURL).Return(batchReadingsJSON, tc.Error) env.On("Flags").Return(&runtime.Flags{}) brew := &Brewfather{} brew.Init(props, env) enabled := brew.Enabled() assert.Equal(t, tc.ExpectedEnabled, enabled, tc.Case) if !enabled { continue } if tc.Template == "" { tc.Template = brew.Template() } assert.Equal(t, tc.ExpectedString, renderTemplate(env, tc.Template, brew), tc.Case) } } ================================================ FILE: src/segments/buf.go ================================================ package segments type Buf struct { Language } func (b *Buf) Template() string { return languageTemplate } func (b *Buf) Enabled() bool { b.extensions = []string{"buf.yaml", "buf.gen.yaml", "buf.work.yaml"} b.tooling = map[string]*cmd{ "buf": { executable: "buf", args: []string{"--version"}, regex: `(?:(?P((?P[0-9]+).(?P[0-9]+).(?P[0-9]+))))`, }, } b.defaultTooling = []string{"buf"} b.versionURLTemplate = "https://github.com/bufbuild/buf/releases/tag/v{{.Full}}" return b.Language.Enabled() } ================================================ FILE: src/segments/buf_test.go ================================================ package segments import ( "fmt" "testing" "github.com/stretchr/testify/assert" ) func TestBuf(t *testing.T) { cases := []struct { Case string ExpectedString string Version string }{ {Case: "Buf 1.12.0", ExpectedString: "1.12.0", Version: "1.12.0"}, } for _, tc := range cases { params := &mockedLanguageParams{ cmd: "buf", versionParam: "--version", versionOutput: tc.Version, extension: "buf.yaml", } env, props := getMockedLanguageEnv(params) b := &Buf{} b.Init(props, env) assert.True(t, b.Enabled(), fmt.Sprintf("Failed in case: %s", tc.Case)) assert.Equal(t, tc.ExpectedString, renderTemplate(env, b.Template(), b), fmt.Sprintf("Failed in case: %s", tc.Case)) } } ================================================ FILE: src/segments/bun.go ================================================ package segments type Bun struct { Language } func (b *Bun) Template() string { return languageTemplate } func (b *Bun) Enabled() bool { b.extensions = []string{"bun.lockb", "bun.lock"} b.tooling = map[string]*cmd{ "bun": { executable: "bun", args: []string{"--version"}, regex: `(?:(?P((?P[0-9]+).(?P[0-9]+).(?P[0-9]+))))`, }, } b.defaultTooling = []string{"bun"} b.versionURLTemplate = "https://github.com/oven-sh/bun/releases/tag/bun-v{{.Full}}" return b.Language.Enabled() } ================================================ FILE: src/segments/bun_test.go ================================================ package segments import ( "fmt" "testing" "github.com/stretchr/testify/assert" ) func TestBun(t *testing.T) { cases := []struct { Case string ExpectedString string Version string }{ {Case: "Bun 1.1.8", ExpectedString: "1.1.8", Version: "1.1.8"}, } for _, tc := range cases { params := &mockedLanguageParams{ cmd: "bun", versionParam: "--version", versionOutput: tc.Version, extension: "bun.lockb", } env, props := getMockedLanguageEnv(params) b := &Bun{} b.Init(props, env) assert.True(t, b.Enabled(), fmt.Sprintf("Failed in case: %s", tc.Case)) assert.Equal(t, tc.ExpectedString, renderTemplate(env, b.Template(), b), fmt.Sprintf("Failed in case: %s", tc.Case)) } } ================================================ FILE: src/segments/carbon_intensity.go ================================================ package segments import ( "encoding/json" "fmt" "github.com/jandedobbeleer/oh-my-posh/src/log" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" ) type CarbonIntensity struct { Base TrendIcon string CarbonIntensityData } type CarbonIntensityResponse struct { Data []CarbonIntensityPeriod `json:"data"` } type CarbonIntensityPeriod struct { Intensity *CarbonIntensityData `json:"intensity"` From string `json:"from"` To string `json:"to"` } type CarbonIntensityData struct { Index Index `json:"index"` Forecast Number `json:"forecast"` Actual Number `json:"actual"` } type Number int func (n Number) String() string { if n == 0 { return "??" } return fmt.Sprintf("%d", n) } type Index string func (i Index) Icon() string { switch i { case "very low": return "↓↓" case "low": return "↓" case "moderate": return "•" case "high": return "↑" case "very high": return "↑↑" default: return "" } } func (d *CarbonIntensity) Enabled() bool { err := d.setStatus() if err != nil { log.Error(err) return false } return true } func (d *CarbonIntensity) Template() string { return " CO₂ {{ .Index.Icon }}{{ .Actual.String }} {{ .TrendIcon }} {{ .Forecast.String }} " } func (d *CarbonIntensity) getResult() (*CarbonIntensityResponse, error) { response := new(CarbonIntensityResponse) url := "https://api.carbonintensity.org.uk/intensity" httpTimeout := d.options.Int(options.HTTPTimeout, options.DefaultHTTPTimeout) body, err := d.env.HTTPRequest(url, nil, httpTimeout) if err != nil { return new(CarbonIntensityResponse), err } err = json.Unmarshal(body, &response) if err != nil { return new(CarbonIntensityResponse), err } return response, nil } func (d *CarbonIntensity) setStatus() error { response, err := d.getResult() if err != nil { return err } if len(response.Data) == 0 { d.Actual = 0 d.Forecast = 0 d.Index = "??" d.TrendIcon = "→" return nil } d.CarbonIntensityData = *response.Data[0].Intensity if d.Forecast > d.Actual { d.TrendIcon = "↗" } if d.Forecast < d.Actual { d.TrendIcon = "↘" } if d.Forecast == d.Actual || d.Actual == 0 || d.Forecast == 0 { d.TrendIcon = "→" } return nil } ================================================ FILE: src/segments/carbon_intensity_test.go ================================================ package segments import ( "errors" "fmt" "testing" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/runtime/mock" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" "github.com/stretchr/testify/assert" ) const ( CARBONINTENSITYURL = "https://api.carbonintensity.org.uk/intensity" ) func TestCarbonIntensitySegmentSingle(t *testing.T) { cases := []struct { Case string Index string ExpectedString string Template string Actual int Forecast int HasError bool HasData bool ExpectedEnabled bool }{ { Case: "Very Low, Going Down", HasError: false, HasData: true, Actual: 20, Forecast: 10, Index: "very low", ExpectedString: "CO₂ ↓↓20 ↘ 10", ExpectedEnabled: true, }, { Case: "Very Low, Staying Same", HasError: false, HasData: true, Actual: 20, Forecast: 20, Index: "very low", ExpectedString: "CO₂ ↓↓20 → 20", ExpectedEnabled: true, }, { Case: "Very Low, Going Up", HasError: false, HasData: true, Actual: 20, Forecast: 30, Index: "very low", ExpectedString: "CO₂ ↓↓20 ↗ 30", ExpectedEnabled: true, }, { Case: "Low, Going Down", HasError: false, HasData: true, Actual: 100, Forecast: 50, Index: "low", ExpectedString: "CO₂ ↓100 ↘ 50", ExpectedEnabled: true, }, { Case: "Low, Staying Same", HasError: false, HasData: true, Actual: 100, Forecast: 100, Index: "low", ExpectedString: "CO₂ ↓100 → 100", ExpectedEnabled: true, }, { Case: "Low, Going Up", HasError: false, HasData: true, Actual: 100, Forecast: 150, Index: "low", ExpectedString: "CO₂ ↓100 ↗ 150", ExpectedEnabled: true, }, { Case: "Moderate, Going Down", HasError: false, HasData: true, Actual: 150, Forecast: 100, Index: "moderate", ExpectedString: "CO₂ •150 ↘ 100", ExpectedEnabled: true, }, { Case: "Moderate, Staying Same", HasError: false, HasData: true, Actual: 150, Forecast: 150, Index: "moderate", ExpectedString: "CO₂ •150 → 150", ExpectedEnabled: true, }, { Case: "Moderate, Going Up", HasError: false, HasData: true, Actual: 150, Forecast: 200, Index: "moderate", ExpectedString: "CO₂ •150 ↗ 200", ExpectedEnabled: true, }, { Case: "High, Going Down", HasError: false, HasData: true, Actual: 200, Forecast: 150, Index: "high", ExpectedString: "CO₂ ↑200 ↘ 150", ExpectedEnabled: true, }, { Case: "High, Staying Same", HasError: false, HasData: true, Actual: 200, Forecast: 200, Index: "high", ExpectedString: "CO₂ ↑200 → 200", ExpectedEnabled: true, }, { Case: "High, Going Up", HasError: false, HasData: true, Actual: 200, Forecast: 300, Index: "high", ExpectedString: "CO₂ ↑200 ↗ 300", ExpectedEnabled: true, }, { Case: "Missing Actual", HasError: false, HasData: true, Actual: 0, // Missing data will be parsed to the default value of 0 Forecast: 300, Index: "high", ExpectedString: "CO₂ ↑?? → 300", ExpectedEnabled: true, }, { Case: "Missing Forecast", HasError: false, HasData: true, Actual: 200, Forecast: 0, // Missing data will be parsed to the default value of 0 Index: "high", ExpectedString: "CO₂ ↑200 → ??", ExpectedEnabled: true, }, { Case: "Missing Index", HasError: false, HasData: true, Actual: 200, Forecast: 300, Index: "", // Missing data will be parsed to the default value of "" ExpectedString: "CO₂ 200 ↗ 300", ExpectedEnabled: true, }, { Case: "Missing Data", HasError: false, HasData: false, Actual: 0, Forecast: 0, Index: "", ExpectedString: "CO₂ ?? → ??", ExpectedEnabled: true, }, { Case: "Error", HasError: true, HasData: false, Actual: 0, Forecast: 0, Index: "", ExpectedString: "", ExpectedEnabled: false, }, } for _, tc := range cases { env := &mock.Environment{} var props = options.Map{ options.HTTPTimeout: 5000, } jsonResponse := fmt.Sprintf( `{ "data": [ { "from": "2023-10-27T12:30Z", "to": "2023-10-27T13:00Z", "intensity": { "forecast": %d, "actual": %d, "index": "%s" } } ] }`, tc.Forecast, tc.Actual, tc.Index, ) if !tc.HasData { jsonResponse = `{ "data": [] }` } if tc.HasError { jsonResponse = `{ "error": "Something went wrong" }` } responseError := errors.New("Something went wrong") if !tc.HasError { responseError = nil } env.On("HTTPRequest", CARBONINTENSITYURL).Return([]byte(jsonResponse), responseError) env.On("Flags").Return(&runtime.Flags{}) d := &CarbonIntensity{} d.Init(props, env) enabled := d.Enabled() assert.Equal(t, tc.ExpectedEnabled, enabled, tc.Case) if !enabled { continue } if tc.Template == "" { tc.Template = d.Template() } assert.Equal(t, tc.ExpectedString, renderTemplate(env, tc.Template, d), tc.Case) } } ================================================ FILE: src/segments/cds.go ================================================ package segments type Cds struct { Language HasDependency bool } func (c *Cds) Template() string { return languageTemplate } func (c *Cds) Enabled() bool { c.extensions = []string{".cdsrc.json", ".cdsrc-private.json", "*.cds"} c.tooling = map[string]*cmd{ "cds": { executable: "cds", args: []string{"--version"}, regex: `@sap/cds: (?:(?P((?P[0-9]+).(?P[0-9]+).(?P[0-9]+))))`, }, } c.defaultTooling = []string{"cds"} c.Language.loadContext = c.loadContext c.Language.inContext = c.inContext c.displayMode = c.options.String(DisplayMode, DisplayModeContext) return c.Language.Enabled() } func (c *Cds) loadContext() { if !c.hasNodePackage("@sap/cds") { return } c.HasDependency = true } func (c *Cds) inContext() bool { return c.HasDependency } ================================================ FILE: src/segments/cds_test.go ================================================ package segments import ( "fmt" "testing" "github.com/stretchr/testify/assert" ) func TestCdsSegment(t *testing.T) { cases := []struct { Case string ExpectedString string Template string Version string PackageJSON string DisplayMode string }{ { Case: "1) cds 5.5.0 - file .cdsrc.json present", ExpectedString: "5.5.0", Version: "@sap/cds: 5.5.0\n@sap/cds-compiler: 2.7.0\n@sap/cds-dk: 4.5.3", DisplayMode: DisplayModeFiles, }, { Case: "2) cds 5.5.1 - file some.cds", ExpectedString: "5.5.1", Version: "@sap/cds: 5.5.1\n@sap/cds-compiler: 2.7.0\n@sap/cds-dk: 4.5.3", DisplayMode: DisplayModeFiles, }, { Case: "4) cds 5.5.3 - package.json dependency", ExpectedString: "5.5.3", Version: "@sap/cds: 5.5.3\n@sap/cds-compiler: 2.7.0\n@sap/cds-dk: 4.5.3", PackageJSON: "{ \"name\": \"my-app\",\"dependencies\": { \"@sap/cds\": \"^5\" } }", DisplayMode: DisplayModeContext, }, { Case: "4) cds 5.5.4 - package.json dependency, major + minor", ExpectedString: "5.5", Template: "{{ .Major }}.{{ .Minor }}", Version: "@sap/cds: 5.5.4\n@sap/cds-compiler: 2.7.0\n@sap/cds-dk: 4.5.3", PackageJSON: "{ \"name\": \"my-app\",\"dependencies\": { \"@sap/cds\": \"^5\" } }", DisplayMode: DisplayModeContext, }, { Case: "6) cds 5.5.9 - display always", ExpectedString: "5.5.9", Version: "@sap/cds: 5.5.9\n@sap/cds-compiler: 2.7.0\n@sap/cds-dk: 4.5.3", PackageJSON: "{ \"name\": \"my-app\",\"dependencies\": { \"@sap/cds\": \"^5\" } }", DisplayMode: DisplayModeAlways, }, { Case: "8) cds 5.5.0 - file .cdsrc-private.json present", ExpectedString: "5.5.0", Version: "@sap/cds: 5.5.0\n@sap/cds-compiler: 2.7.0\n@sap/cds-dk: 4.5.3", DisplayMode: DisplayModeFiles, }, } for _, tc := range cases { params := &mockedLanguageParams{ cmd: "cds", versionParam: "--version", versionOutput: tc.Version, extension: ".cdsrc.json", } env, props := getMockedLanguageEnv(params) if tc.DisplayMode == "" { tc.DisplayMode = DisplayModeContext } props[DisplayMode] = tc.DisplayMode env.On("HasFiles", "package.json").Return(len(tc.PackageJSON) != 0) env.On("FileContent", "package.json").Return(tc.PackageJSON) cds := &Cds{} cds.Init(props, env) if tc.Template == "" { tc.Template = cds.Template() } failMsg := fmt.Sprintf("Failed in case: %s", tc.Case) assert.True(t, cds.Enabled(), failMsg) assert.Equal(t, tc.ExpectedString, renderTemplate(env, tc.Template, cds), failMsg) } } ================================================ FILE: src/segments/cf.go ================================================ package segments type Cf struct { Language } func (c *Cf) Template() string { return languageTemplate } func (c *Cf) Enabled() bool { c.extensions = []string{"manifest.yml", "mta.yaml"} c.tooling = map[string]*cmd{ "cf": { executable: "cf", args: []string{"version"}, regex: `(?:(?P((?P[0-9]+).(?P[0-9]+).(?P[0-9]+))))`, }, } c.defaultTooling = []string{"cf"} c.displayMode = c.options.String(DisplayMode, DisplayModeFiles) c.versionURLTemplate = "https://github.com/cloudfoundry/cli/releases/tag/v{{ .Full }}" return c.Language.Enabled() } ================================================ FILE: src/segments/cf_target.go ================================================ package segments import ( "errors" "strings" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" ) type CfTarget struct { Base CfTargetDetails } type CfTargetDetails struct { URL string User string Org string Space string } func (c *CfTarget) Template() string { return "{{if .Org }}{{ .Org }}{{ end }}{{if .Space }}/{{ .Space }}{{ end }}" } func (c *CfTarget) Enabled() bool { if !c.env.HasCommand("cf") { return false } displayMode := c.options.String(DisplayMode, DisplayModeAlways) if displayMode != DisplayModeFiles { return c.setCFTargetStatus() } files := c.options.StringArray(options.Files, []string{"manifest.yml"}) for _, file := range files { manifest, err := c.env.HasParentFilePath(file, false) if err != nil || manifest.IsDir { continue } return c.setCFTargetStatus() } return false } func (c *CfTarget) setCFTargetStatus() bool { output, err := c.getCFTargetCommandOutput() if err != nil { return false } lines := strings.SplitSeq(output, "\n") for line := range lines { key, value, found := strings.Cut(line, ":") if !found { continue } value = strings.TrimSpace(value) switch key { case "API endpoint": c.URL = value case "user": c.User = value case "org": c.Org = value case "space": c.Space = value } } return true } func (c *CfTarget) getCFTargetCommandOutput() (string, error) { output, err := c.env.RunCommand("cf", "target") if err != nil { return "", err } if output == "" { return "", errors.New("cf command output is empty") } return output, nil } ================================================ FILE: src/segments/cf_target_test.go ================================================ package segments import ( "errors" "fmt" "os/exec" "testing" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/runtime/mock" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" "github.com/stretchr/testify/assert" ) func TestCFTargetSegment(t *testing.T) { cases := []struct { CommandError error FileInfo *runtime.FileInfo Case string Template string ExpectedString string DisplayMode string TargetOutput string }{ { Case: "not logged in to CF account", TargetOutput: `Not logged in`, CommandError: &exec.ExitError{}, }, { Case: "logged in, default template", ExpectedString: "12345678trial/dev", TargetOutput: "API endpoint: https://api.cf.eu10.hana.ondemand.com\nAPI version: 3.109.0\nuser: user@some.com\norg: 12345678trial\nspace: dev", }, { Case: "no output from command", }, { Case: "logged in, full template", Template: "{{.URL}} {{.User}} {{.Org}} {{.Space}}", ExpectedString: "https://api.cf.eu10.hana.ondemand.com user@some.com 12345678trial dev", TargetOutput: "API endpoint: https://api.cf.eu10.hana.ondemand.com\nAPI version: 3.109.0\nuser: user@some.com\norg: 12345678trial\nspace: dev", }, { Case: "files and no manifest file", DisplayMode: DisplayModeFiles, TargetOutput: "API endpoint: https://api.cf.eu10.hana.ondemand.com\nAPI version: 3.109.0\nuser: user@some.com\norg: 12345678trial\nspace: dev", }, { Case: "files and a manifest file", ExpectedString: "12345678trial/dev", DisplayMode: DisplayModeFiles, FileInfo: &runtime.FileInfo{}, TargetOutput: "API endpoint: https://api.cf.eu10.hana.ondemand.com\nAPI version: 3.109.0\nuser: user@some.com\norg: 12345678trial\nspace: dev", }, { Case: "files and a manifest directory", DisplayMode: DisplayModeFiles, FileInfo: &runtime.FileInfo{ IsDir: true, }, }, } for _, tc := range cases { var env = new(mock.Environment) env.On("HasCommand", "cf").Return(true) env.On("RunCommand", "cf", []string{"target"}).Return(tc.TargetOutput, tc.CommandError) env.On("Pwd", nil).Return("/usr/home/dev/my-app") env.On("Home", nil).Return("/usr/home") var err error if tc.FileInfo == nil { err = errors.New("no such file or directory") } env.On("HasParentFilePath", "manifest.yml", false).Return(tc.FileInfo, err) cfTarget := &CfTarget{} props := options.Map{ DisplayMode: tc.DisplayMode, } if tc.Template == "" { tc.Template = cfTarget.Template() } cfTarget.Init(props, env) failMsg := fmt.Sprintf("Failed in case: %s", tc.Case) assert.Equal(t, len(tc.ExpectedString) > 0, cfTarget.Enabled(), failMsg) assert.Equal(t, tc.ExpectedString, renderTemplate(env, tc.Template, cfTarget), failMsg) } } ================================================ FILE: src/segments/cf_test.go ================================================ package segments import ( "fmt" "testing" "github.com/stretchr/testify/assert" ) func TestCFSegment(t *testing.T) { cases := []struct { Case string Template string ExpectedString string CfYamlFile string Version string DisplayMode string }{ { Case: "1) cf 2.12.1 - file manifest.yml", ExpectedString: "2.12.1", CfYamlFile: "manifest.yml", Version: `cf.exe version 2.12.1+645c3ce6a.2021-08-16`, DisplayMode: DisplayModeFiles, }, { Case: "2) cf 11.0.0-rc1 - file mta.yaml", Template: "{{ .Major }}", ExpectedString: "11", CfYamlFile: "mta.yaml", Version: `cf version 11.0.0-rc1`, DisplayMode: DisplayModeFiles, }, { Case: "4) cf 11.1.0-rc1 - mode always", Template: "{{ .Major }}.{{ .Minor }}", ExpectedString: "11.1", Version: `cf.exe version 11.1.0-rc1`, DisplayMode: DisplayModeAlways, }, } for _, tc := range cases { params := &mockedLanguageParams{ cmd: "cf", versionParam: "version", versionOutput: tc.Version, extension: "manifest.yml", } env, props := getMockedLanguageEnv(params) props[DisplayMode] = tc.DisplayMode cf := &Cf{} cf.Init(props, env) if tc.Template == "" { tc.Template = cf.Template() } failMsg := fmt.Sprintf("Failed in case: %s", tc.Case) assert.True(t, cf.Enabled(), failMsg) assert.Equal(t, tc.ExpectedString, renderTemplate(env, tc.Template, cf), failMsg) } } ================================================ FILE: src/segments/claude.go ================================================ package segments import ( "fmt" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/log" "github.com/jandedobbeleer/oh-my-posh/src/text" ) // Claude segment displays Claude Code session information type Claude struct { Base ClaudeData } // ClaudeData represents the parsed Claude JSON data type ClaudeData struct { Model ClaudeModel `json:"model"` Workspace ClaudeWorkspace `json:"workspace"` SessionID string `json:"session_id"` ContextWindow ClaudeContextWindow `json:"context_window"` Cost ClaudeCost `json:"cost"` } // ClaudeModel represents the AI model information type ClaudeModel struct { ID string `json:"id"` DisplayName string `json:"display_name"` } // ClaudeWorkspace represents workspace directory information type ClaudeWorkspace struct { CurrentDir string `json:"current_dir"` ProjectDir string `json:"project_dir"` } // ClaudeCost represents cost and duration information type ClaudeCost struct { TotalCostUSD float64 `json:"total_cost_usd"` TotalDurationMS int64 `json:"total_duration_ms"` } // ClaudeContextWindow represents token usage information type ClaudeContextWindow struct { UsedPercentage *int `json:"used_percentage"` RemainingPercentage *int `json:"remaining_percentage"` CurrentUsage *ClaudeCurrentUsage `json:"current_usage"` TotalInputTokens int `json:"total_input_tokens"` TotalOutputTokens int `json:"total_output_tokens"` ContextWindowSize int `json:"context_window_size"` } // ClaudeCurrentUsage represents current context window usage from the last API call type ClaudeCurrentUsage struct { InputTokens int `json:"input_tokens"` OutputTokens int `json:"output_tokens"` CacheCreationInputTokens int `json:"cache_creation_input_tokens"` CacheReadInputTokens int `json:"cache_read_input_tokens"` } const ( thousand = 1000.0 million = 1000000.0 ) func (c *Claude) Template() string { return " \U000f0bc9 {{ .Model.DisplayName }} \uf2d0 {{ .TokenUsagePercent.Gauge }} " } func (c *Claude) Enabled() bool { log.Debug("claude segment: checking if enabled") // Try to get Claude data from session cache claudeData, found := cache.Get[ClaudeData](cache.Session, cache.CLAUDECACHE) if !found { log.Debug("claude segment: no Claude data found in session cache") return false } log.Debug("claude segment: found Claude data in session cache") log.Debugf("claude segment: model=%s, session=%s", claudeData.Model.DisplayName, claudeData.SessionID) // Copy the data to our embedded struct c.ClaudeData = claudeData return true } // TokenUsagePercent returns the percentage of context window used. // Uses pre-calculated UsedPercentage when available (resets on compact/clear), // falls back to calculating from CurrentUsage, then to total tokens for backwards compatibility. func (c *Claude) TokenUsagePercent() text.Percentage { // Prefer pre-calculated UsedPercentage - most accurate and resets on compact/clear // When UsedPercentage is nil (null in JSON), context was reset - return 0 if c.ContextWindow.UsedPercentage != nil { if *c.ContextWindow.UsedPercentage > 100 { return 100 } return text.Percentage(*c.ContextWindow.UsedPercentage) } // UsedPercentage is nil - check if CurrentUsage is also nil (indicates reset/clear) if c.ContextWindow.CurrentUsage == nil { return 0 } if c.ContextWindow.ContextWindowSize <= 0 { return 0 } // Calculate from CurrentUsage (includes cache tokens for accurate context measurement) currentTokens := c.ContextWindow.CurrentUsage.InputTokens + c.ContextWindow.CurrentUsage.CacheCreationInputTokens + c.ContextWindow.CurrentUsage.CacheReadInputTokens // Fallback to total tokens if CurrentUsage is not provided (backwards compatibility) if currentTokens <= 0 { currentTokens = c.ContextWindow.TotalInputTokens + c.ContextWindow.TotalOutputTokens } if currentTokens <= 0 { return 0 } // Use floating-point arithmetic for accurate percentage calculation percent := (float64(currentTokens) * 100.0) / float64(c.ContextWindow.ContextWindowSize) // Round to nearest integer and cap at 100 roundedPercent := int(percent + 0.5) if roundedPercent > 100 { return 100 } return text.Percentage(roundedPercent) } // FormattedCost returns the cost formatted as a currency string func (c *Claude) FormattedCost() string { if c.Cost.TotalCostUSD < 0.01 { return fmt.Sprintf("$%.4f", c.Cost.TotalCostUSD) } return fmt.Sprintf("$%.2f", c.Cost.TotalCostUSD) } // FormattedTokens returns a human-readable string of current context tokens. // Uses CurrentUsage (which represents actual context and resets on compact/clear) // with fallback to total tokens for backwards compatibility. func (c *Claude) FormattedTokens() string { var currentTokens int // Use CurrentUsage for display - includes cache tokens for accurate context measurement // When CurrentUsage is nil (context reset), fall back to total tokens if c.ContextWindow.CurrentUsage != nil { currentTokens = c.ContextWindow.CurrentUsage.InputTokens + c.ContextWindow.CurrentUsage.CacheCreationInputTokens + c.ContextWindow.CurrentUsage.CacheReadInputTokens } // Fallback to total tokens if CurrentUsage is not provided (backwards compatibility) if currentTokens <= 0 { currentTokens = c.ContextWindow.TotalInputTokens + c.ContextWindow.TotalOutputTokens } if currentTokens < int(thousand) { return fmt.Sprintf("%d", currentTokens) } if currentTokens < int(million) { return fmt.Sprintf("%.1fK", float64(currentTokens)/thousand) } return fmt.Sprintf("%.1fM", float64(currentTokens)/million) } ================================================ FILE: src/segments/claude_test.go ================================================ package segments import ( "testing" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/runtime/mock" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" "github.com/jandedobbeleer/oh-my-posh/src/text" "github.com/stretchr/testify/assert" ) func TestClaudeSegment(t *testing.T) { cases := []struct { Case string ClaudeData *ClaudeData ExpectedModel string ExpectedSession string ExpectedEnabled bool }{ { Case: "No cache data", ClaudeData: nil, ExpectedEnabled: false, }, { Case: "Valid cache data with all fields", ClaudeData: &ClaudeData{ SessionID: "abc123", Model: ClaudeModel{ ID: "claude-opus-4-1", DisplayName: "Opus", }, Workspace: ClaudeWorkspace{ CurrentDir: "/repo/project", ProjectDir: "/repo", }, Cost: ClaudeCost{ TotalCostUSD: 0.01, TotalDurationMS: 45000, }, ContextWindow: ClaudeContextWindow{ TotalInputTokens: 15234, TotalOutputTokens: 4521, ContextWindowSize: 200000, CurrentUsage: &ClaudeCurrentUsage{ InputTokens: 8500, OutputTokens: 1200, }, }, }, ExpectedEnabled: true, ExpectedModel: "Opus", ExpectedSession: "abc123", }, { Case: "Valid cache data with partial fields", ClaudeData: &ClaudeData{ SessionID: "xyz789", Model: ClaudeModel{ ID: "claude-sonnet-3-5", DisplayName: "Sonnet 3.5", }, ContextWindow: ClaudeContextWindow{ TotalInputTokens: 1000, TotalOutputTokens: 500, ContextWindowSize: 100000, }, }, ExpectedEnabled: true, ExpectedModel: "Sonnet 3.5", ExpectedSession: "xyz789", }, } for _, tc := range cases { // Setup cache for test if tc.ClaudeData != nil { cache.Set(cache.Session, cache.CLAUDECACHE, *tc.ClaudeData, cache.INFINITE) } else { cache.Delete(cache.Session, cache.CLAUDECACHE) } env := new(mock.Environment) claude := &Claude{ Base: Base{ env: env, options: options.Map{}, }, } enabled := claude.Enabled() assert.Equal(t, tc.ExpectedEnabled, enabled, tc.Case) if tc.ExpectedEnabled { assert.Equal(t, tc.ExpectedModel, claude.Model.DisplayName, tc.Case) assert.Equal(t, tc.ExpectedSession, claude.SessionID, tc.Case) } } } func TestClaudeTokenUsagePercent(t *testing.T) { cases := []struct { UsedPercentage *int Case string InputTokens int OutputTokens int CurrentInput int CacheCreationInputTokens int CacheReadInputTokens int ContextWindow int ExpectedPercent text.Percentage HasCurrentUsage bool }{ { Case: "Uses UsedPercentage when available", UsedPercentage: new(42), ContextWindow: 200000, ExpectedPercent: 42, }, { Case: "UsedPercentage capped at 100", UsedPercentage: new(150), ContextWindow: 200000, ExpectedPercent: 100, }, { Case: "UsedPercentage zero is valid", UsedPercentage: new(0), ContextWindow: 200000, ExpectedPercent: 0, }, { Case: "Context reset - both UsedPercentage and CurrentUsage nil", UsedPercentage: nil, HasCurrentUsage: false, InputTokens: 50000, // High cumulative total - should be ignored OutputTokens: 50000, ContextWindow: 200000, ExpectedPercent: 0, // Should return 0 after reset, not fallback to total }, { Case: "Zero context window (no UsedPercentage)", HasCurrentUsage: true, InputTokens: 1000, OutputTokens: 500, ContextWindow: 0, ExpectedPercent: 0, }, { Case: "10% usage (fallback to total)", HasCurrentUsage: true, InputTokens: 8000, OutputTokens: 2000, ContextWindow: 100000, ExpectedPercent: 10, }, { Case: "50% usage (fallback to total)", HasCurrentUsage: true, InputTokens: 50000, OutputTokens: 50000, ContextWindow: 200000, ExpectedPercent: 50, }, { Case: "Over 100% usage (capped)", HasCurrentUsage: true, InputTokens: 100000, OutputTokens: 50000, ContextWindow: 100000, ExpectedPercent: 100, }, { Case: "Uses CurrentUsage input tokens", HasCurrentUsage: true, InputTokens: 100000, // High cumulative total OutputTokens: 50000, CurrentInput: 20000, // Current context input ContextWindow: 200000, ExpectedPercent: 10, // Should use current input (20000/200000 = 10%) }, { Case: "Uses CurrentUsage with cache tokens", HasCurrentUsage: true, InputTokens: 100000, // High cumulative total OutputTokens: 50000, CurrentInput: 10000, CacheCreationInputTokens: 5000, CacheReadInputTokens: 5000, ContextWindow: 200000, ExpectedPercent: 10, // (10000+5000+5000)/200000 = 10% }, { Case: "Uses CurrentUsage after compact (low current, high total)", HasCurrentUsage: true, InputTokens: 100000, // High cumulative total OutputTokens: 50000, CurrentInput: 6000, // Low current context (after compact) ContextWindow: 200000, ExpectedPercent: 3, // Should use current (6000/200000 = 3%) }, { Case: "Fallback to total when CurrentUsage is zero", HasCurrentUsage: true, InputTokens: 20000, OutputTokens: 10000, CurrentInput: 0, ContextWindow: 100000, ExpectedPercent: 30, // Should fallback to total (30000/100000 = 30%) }, } for _, tc := range cases { claude := &Claude{} claude.ContextWindow.TotalInputTokens = tc.InputTokens claude.ContextWindow.TotalOutputTokens = tc.OutputTokens if tc.HasCurrentUsage { claude.ContextWindow.CurrentUsage = &ClaudeCurrentUsage{ InputTokens: tc.CurrentInput, CacheCreationInputTokens: tc.CacheCreationInputTokens, CacheReadInputTokens: tc.CacheReadInputTokens, } } claude.ContextWindow.UsedPercentage = tc.UsedPercentage claude.ContextWindow.ContextWindowSize = tc.ContextWindow percent := claude.TokenUsagePercent() assert.Equal(t, tc.ExpectedPercent, percent, tc.Case) } } func TestClaudeFormattedCost(t *testing.T) { cases := []struct { Case string ExpectedCost string CostUSD float64 }{ { Case: "Very small cost", CostUSD: 0.0012, ExpectedCost: "$0.0012", }, { Case: "Small cost", CostUSD: 0.0099, ExpectedCost: "$0.0099", }, { Case: "Regular cost", CostUSD: 0.15, ExpectedCost: "$0.15", }, { Case: "Large cost", CostUSD: 12.34, ExpectedCost: "$12.34", }, } for _, tc := range cases { claude := &Claude{} claude.Cost.TotalCostUSD = tc.CostUSD formatted := claude.FormattedCost() assert.Equal(t, tc.ExpectedCost, formatted, tc.Case) } } func TestClaudeFormattedTokens(t *testing.T) { cases := []struct { Case string ExpectedFormat string InputTokens int OutputTokens int CurrentInput int CacheCreationInputTokens int CacheReadInputTokens int HasCurrentUsage bool }{ { Case: "Small token count (fallback to total)", HasCurrentUsage: true, InputTokens: 300, OutputTokens: 200, ExpectedFormat: "500", }, { Case: "Thousands (fallback to total)", HasCurrentUsage: true, InputTokens: 8500, OutputTokens: 1500, ExpectedFormat: "10.0K", }, { Case: "Tens of thousands (fallback to total)", HasCurrentUsage: true, InputTokens: 50000, OutputTokens: 25000, ExpectedFormat: "75.0K", }, { Case: "Millions (fallback to total)", HasCurrentUsage: true, InputTokens: 1500000, OutputTokens: 500000, ExpectedFormat: "2.0M", }, { Case: "Uses CurrentUsage input tokens", HasCurrentUsage: true, InputTokens: 100000, // High cumulative total OutputTokens: 50000, CurrentInput: 10000, // Current context input ExpectedFormat: "10.0K", }, { Case: "Uses CurrentUsage with cache tokens", HasCurrentUsage: true, InputTokens: 100000, // High cumulative total OutputTokens: 50000, CurrentInput: 5000, CacheCreationInputTokens: 2500, CacheReadInputTokens: 2500, ExpectedFormat: "10.0K", // 5000+2500+2500 = 10000 }, { Case: "Uses CurrentUsage after compact (low current)", HasCurrentUsage: true, InputTokens: 500000, // High cumulative total OutputTokens: 200000, CurrentInput: 500, // Low current context (after compact) ExpectedFormat: "500", }, { Case: "Fallback to total when CurrentUsage is zero", HasCurrentUsage: true, InputTokens: 50000, OutputTokens: 25000, CurrentInput: 0, ExpectedFormat: "75.0K", // Should fallback to total }, { Case: "Nil CurrentUsage falls back to total", HasCurrentUsage: false, InputTokens: 50000, OutputTokens: 25000, ExpectedFormat: "75.0K", // Should fallback to total }, } for _, tc := range cases { claude := &Claude{} claude.ContextWindow.TotalInputTokens = tc.InputTokens claude.ContextWindow.TotalOutputTokens = tc.OutputTokens if tc.HasCurrentUsage { claude.ContextWindow.CurrentUsage = &ClaudeCurrentUsage{ InputTokens: tc.CurrentInput, CacheCreationInputTokens: tc.CacheCreationInputTokens, CacheReadInputTokens: tc.CacheReadInputTokens, } } formatted := claude.FormattedTokens() assert.Equal(t, tc.ExpectedFormat, formatted, tc.Case) } } ================================================ FILE: src/segments/clojure.go ================================================ package segments type Clojure struct { Language } func (c *Clojure) Template() string { return languageTemplate } func (c *Clojure) Enabled() bool { c.init() return c.Language.Enabled() } func (c *Clojure) init() { options := c.options.StringArray(Tooling, []string{}) if len(options) == 0 { c.defaultTooling = []string{"clojure", "lein"} } c.extensions = []string{ "project.clj", "deps.edn", "build.boot", "bb.edn", "*.clj", "*.cljc", "*.cljs", } c.tooling = map[string]*cmd{ "clojure": { executable: "clojure", args: []string{"--version"}, regex: `Clojure CLI version (?P(?P[0-9]+)\.(?P[0-9]+)\.(?P[0-9]+)(?:\.(?P[0-9]+))?)`, }, "lein": { executable: "lein", args: []string{"--version"}, regex: `Leiningen (?P(?P[0-9]+)\.(?P[0-9]+)\.(?P[0-9]+))`, }, } } ================================================ FILE: src/segments/clojure_test.go ================================================ package segments import ( "fmt" "testing" "github.com/stretchr/testify/assert" ) func TestClojure(t *testing.T) { cases := []struct { Case string ExpectedString string Version string Cmd string }{ { Case: "Clojure CLI 1.11.1.1113", ExpectedString: "1.11.1.1113", Version: "Clojure CLI version 1.11.1.1113", Cmd: "clojure", }, { Case: "Leiningen 2.9.8", ExpectedString: "2.9.8", Version: "Leiningen 2.9.8 on Java 11.0.11 OpenJDK 64-Bit Server VM", Cmd: "lein", }, } for _, tc := range cases { params := &mockedLanguageParams{ cmd: tc.Cmd, versionParam: "--version", versionOutput: tc.Version, extension: "*.clj", } env, props := getMockedLanguageEnv(params) props[LanguageExtensions] = []string{params.extension} if tc.Cmd != "clojure" { env.On("HasCommand", "clojure").Return(false) } c := &Clojure{} c.Init(props, env) assert.True(t, c.Enabled(), fmt.Sprintf("Failed in case: %s", tc.Case)) assert.Equal(t, tc.ExpectedString, renderTemplate(env, c.Template(), c), fmt.Sprintf("Failed in case: %s", tc.Case)) } } ================================================ FILE: src/segments/cmake.go ================================================ package segments type Cmake struct { Language } func (c *Cmake) Template() string { return languageTemplate } func (c *Cmake) Enabled() bool { c.extensions = []string{"*.cmake", "CMakeLists.txt"} c.tooling = map[string]*cmd{ "cmake": { executable: "cmake", args: []string{"--version"}, regex: `cmake version (?P((?P[0-9]+).(?P[0-9]+).(?P[0-9]+)))`, }, } c.defaultTooling = []string{"cmake"} c.versionURLTemplate = "https://cmake.org/cmake/help/v{{ .Major }}.{{ .Minor }}" return c.Language.Enabled() } ================================================ FILE: src/segments/cmake_test.go ================================================ package segments import ( "fmt" "testing" "github.com/stretchr/testify/assert" ) func TestCmake(t *testing.T) { cases := []struct { Case string ExpectedString string Version string }{ {Case: "Cmake 3.23.2", ExpectedString: "3.23.2", Version: "cmake version 3.23.2"}, {Case: "Cmake 2.3.13", ExpectedString: "2.3.12", Version: "cmake version 2.3.12"}, {Case: "", ExpectedString: "err parsing info from cmake with", Version: ""}, } for _, tc := range cases { params := &mockedLanguageParams{ cmd: "cmake", versionParam: "--version", versionOutput: tc.Version, extension: "*.cmake", } env, props := getMockedLanguageEnv(params) c := &Cmake{} c.Init(props, env) failMsg := fmt.Sprintf("Failed in case: %s", tc.Case) assert.True(t, c.Enabled(), failMsg) assert.Equal(t, tc.ExpectedString, renderTemplate(env, c.Template(), c), failMsg) } } ================================================ FILE: src/segments/connection.go ================================================ package segments import ( "strings" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" ) type Connection struct { Base runtime.Connection } const ( Type options.Option = "type" ) func (c *Connection) Template() string { return " {{ if eq .Type \"wifi\"}}\uf1eb{{ else if eq .Type \"ethernet\"}}\ueba9{{ end }} " } func (c *Connection) Enabled() bool { types := c.options.String(Type, "wifi|ethernet") connectionTypes := strings.SplitSeq(types, "|") for connectionType := range connectionTypes { network, err := c.env.Connection(runtime.ConnectionType(connectionType)) if err != nil { continue } c.Connection = *network return true } return false } ================================================ FILE: src/segments/connection_test.go ================================================ package segments import ( "fmt" "testing" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/runtime/mock" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" "github.com/stretchr/testify/assert" ) func TestConnection(t *testing.T) { type connectionResponse struct { Connection *runtime.Connection Error error } cases := []struct { Case string ExpectedString string ConnectionType string Connections []*connectionResponse ExpectedEnabled bool }{ { Case: "WiFi only, enabled", ExpectedString: "\uf1eb", ExpectedEnabled: true, ConnectionType: "wifi", Connections: []*connectionResponse{ { Connection: &runtime.Connection{ Name: "WiFi", Type: "wifi", }, }, }, }, { Case: "WiFi only, disabled", ConnectionType: "wifi", Connections: []*connectionResponse{ { Connection: &runtime.Connection{ Type: runtime.WIFI, }, Error: fmt.Errorf("no connection"), }, }, }, { Case: "WiFi and Ethernet, enabled", ConnectionType: "wifi|ethernet", ExpectedString: "\ueba9", ExpectedEnabled: true, Connections: []*connectionResponse{ { Connection: &runtime.Connection{ Type: runtime.WIFI, }, Error: fmt.Errorf("no connection"), }, { Connection: &runtime.Connection{ Type: runtime.ETHERNET, }, }, }, }, { Case: "WiFi and Ethernet, disabled", ConnectionType: "wifi|ethernet", Connections: []*connectionResponse{ { Connection: &runtime.Connection{ Type: runtime.WIFI, }, Error: fmt.Errorf("no connection"), }, { Connection: &runtime.Connection{ Type: runtime.ETHERNET, }, Error: fmt.Errorf("no connection"), }, }, }, } for _, tc := range cases { env := &mock.Environment{} for _, con := range tc.Connections { env.On("Connection", con.Connection.Type).Return(con.Connection, con.Error) } props := &options.Map{ Type: tc.ConnectionType, } c := &Connection{} c.Init(props, env) assert.Equal(t, tc.ExpectedEnabled, c.Enabled(), fmt.Sprintf("Failed in case: %s", tc.Case)) if tc.ExpectedEnabled { assert.Equal(t, tc.ExpectedString, renderTemplate(env, c.Template(), c), fmt.Sprintf("Failed in case: %s", tc.Case)) } } } ================================================ FILE: src/segments/copilot.go ================================================ package segments import ( "encoding/json" "net/http" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/cli/auth" "github.com/jandedobbeleer/oh-my-posh/src/log" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" "github.com/jandedobbeleer/oh-my-posh/src/text" ) // CopilotUsage represents usage statistics for a specific quota type. type CopilotUsage struct { Used int `json:"used"` Limit int `json:"limit"` Percent text.Percentage `json:"percent"` Remaining text.Percentage `json:"remaining"` Unlimited bool `json:"unlimited"` } // Copilot displays GitHub Copilot usage statistics. type Copilot struct { Base BillingCycleEnd string `json:"billing_cycle_end"` Premium CopilotUsage `json:"premium"` Inline CopilotUsage `json:"inline"` Chat CopilotUsage `json:"chat"` } const ( copilotAPIURL = "https://api.github.com/copilot_internal/user" ) // copilotQuotaSnapshot represents a single quota type. type copilotQuotaSnapshot struct { Entitlement int `json:"entitlement"` Remaining int `json:"remaining"` Unlimited bool `json:"unlimited"` } // copilotQuotaSnapshots represents the quota snapshots structure. type copilotQuotaSnapshots struct { PremiumInteractions copilotQuotaSnapshot `json:"premium_interactions"` Completions copilotQuotaSnapshot `json:"completions"` Chat copilotQuotaSnapshot `json:"chat"` } // copilotAPIResponse represents the API response structure. type copilotAPIResponse struct { QuotaSnapshots *copilotQuotaSnapshots `json:"quota_snapshots"` QuotaResetDate string `json:"quota_reset_date"` QuotaResetDateUTC string `json:"quota_reset_date_utc"` } func (c *Copilot) Template() string { return " \uec1e {{ .Premium.Percent.Gauge }} " } func (c *Copilot) Enabled() bool { err := c.setStatus() if err != nil { log.Error(err) return false } return true } func (c *Copilot) getAccessToken() string { // Check cache from `oh-my-posh auth copilot` if cachedToken, OK := cache.Get[string](cache.Device, auth.CopilotTokenKey); OK && len(cachedToken) != 0 { return cachedToken } return "" } func (c *Copilot) getResult() (*copilotAPIResponse, error) { accessToken := c.getAccessToken() if len(accessToken) == 0 { return nil, &noAccessTokenError{} } log.Debug("found access token") httpTimeout := c.options.Int(options.HTTPTimeout, options.DefaultHTTPTimeout) addAuthHeader := func(request *http.Request) { request.Header.Set("Authorization", "Bearer "+accessToken) request.Header.Set("User-Agent", "GitHub-Copilot-Usage-Tray") request.Header.Set("Accept", "application/json") request.Header.Set("Content-Type", "application/json") } body, err := c.env.HTTPRequest(copilotAPIURL, nil, httpTimeout, addAuthHeader) if err != nil { log.Error(err) return nil, err } log.Debug("executed HTTP request successfully") response := new(copilotAPIResponse) err = json.Unmarshal(body, &response) if err != nil { return nil, err } return response, nil } func (c *Copilot) setStatus() error { response, err := c.getResult() if err != nil { return err } // Extract quota data from response - try different paths quotaSnapshots := c.extractQuotaSnapshots(response) if quotaSnapshots == nil { return &noQuotaDataError{} } // Calculate premium usage c.Premium = c.calculateUsage(quotaSnapshots.PremiumInteractions) // Calculate inline usage (completions) c.Inline = c.calculateUsage(quotaSnapshots.Completions) // Calculate chat usage c.Chat = c.calculateUsage(quotaSnapshots.Chat) // Set billing cycle end date c.BillingCycleEnd = response.QuotaResetDate if c.BillingCycleEnd == "" { c.BillingCycleEnd = response.QuotaResetDateUTC } return nil } func (c *Copilot) extractQuotaSnapshots(response *copilotAPIResponse) *copilotQuotaSnapshots { if response == nil { return nil } // Use root-level quota_snapshots if response.QuotaSnapshots != nil { return response.QuotaSnapshots } return nil } func (c *Copilot) calculateUsage(snapshot copilotQuotaSnapshot) CopilotUsage { if snapshot.Unlimited { return CopilotUsage{ Used: 0, Limit: 0, Percent: text.Percentage(0), Remaining: text.Percentage(100), Unlimited: true, } } used := max(snapshot.Entitlement-snapshot.Remaining, 0) percent := c.calculatePercent(used, snapshot.Entitlement) remainingPercent := 100 - percent return CopilotUsage{ Used: used, Limit: snapshot.Entitlement, Percent: text.Percentage(percent), Remaining: text.Percentage(remainingPercent), Unlimited: false, } } func (c *Copilot) calculatePercent(used, limit int) int { if limit <= 0 { return 0 } percent := (used * 100) / limit if percent > 100 { return 100 } return percent } // Custom error types for better error handling type noQuotaDataError struct{} func (e *noQuotaDataError) Error() string { return "no quota data in response" } type noAccessTokenError struct{} func (e *noAccessTokenError) Error() string { return "no access token available, use 'oh-my-posh auth copilot' to authenticate" } ================================================ FILE: src/segments/copilot_test.go ================================================ package segments import ( "errors" "testing" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/cli/auth" "github.com/jandedobbeleer/oh-my-posh/src/runtime/mock" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" "github.com/jandedobbeleer/oh-my-posh/src/text" "github.com/stretchr/testify/assert" ) const ( copilotTestURL = "https://api.github.com/copilot_internal/user" ) func TestCopilotSegment(t *testing.T) { cases := []struct { Case string JSONResponse string ExpectedString string Template string HasToken bool ExpectedEnabled bool HasError bool }{ { Case: "Valid response with usage data", JSONResponse: `{ "quota_snapshots": { "premium_interactions": { "entitlement": 50, "remaining": 35, "unlimited": false }, "completions": { "entitlement": 2000, "remaining": 1500, "unlimited": false }, "chat": { "entitlement": 100, "remaining": 80, "unlimited": false } }, "quota_reset_date": "2025-02-01T00:00:00Z" }`, Template: " \uec1e {{ .Premium.Used }}/{{ .Premium.Limit }} | {{ .Inline.Used }}/{{ .Inline.Limit }} | {{ .Chat.Used }}/{{ .Chat.Limit }} ", ExpectedString: "\uec1e 15/50 | 500/2000 | 20/100", ExpectedEnabled: true, HasToken: true, }, { Case: "Full premium usage", JSONResponse: `{ "quota_snapshots": { "premium_interactions": { "entitlement": 50, "remaining": 0, "unlimited": false }, "completions": { "entitlement": 2000, "remaining": 0, "unlimited": false }, "chat": { "entitlement": 100, "remaining": 0, "unlimited": false } }, "quota_reset_date": "2025-02-01T00:00:00Z" }`, Template: " \uec1e {{ .Premium.Used }}/{{ .Premium.Limit }} | {{ .Inline.Used }}/{{ .Inline.Limit }} | {{ .Chat.Used }}/{{ .Chat.Limit }} ", ExpectedString: "\uec1e 50/50 | 2000/2000 | 100/100", ExpectedEnabled: true, HasToken: true, }, { Case: "No usage", JSONResponse: `{ "quota_snapshots": { "premium_interactions": { "entitlement": 50, "remaining": 50, "unlimited": false }, "completions": { "entitlement": 2000, "remaining": 2000, "unlimited": false }, "chat": { "entitlement": 0, "remaining": 0, "unlimited": true } }, "quota_reset_date": "2025-02-01T00:00:00Z" }`, Template: " \uec1e {{ .Premium.Used }}/{{ .Premium.Limit }} | {{ .Inline.Used }}/{{ .Inline.Limit }} | {{ .Chat.Used }}/{{ .Chat.Limit }} ", ExpectedString: "\uec1e 0/50 | 0/2000 | 0/0", ExpectedEnabled: true, HasToken: true, }, { Case: "Custom template with percentages", JSONResponse: `{ "quota_snapshots": { "premium_interactions": { "entitlement": 100, "remaining": 50, "unlimited": false }, "completions": { "entitlement": 1000, "remaining": 750, "unlimited": false }, "chat": { "entitlement": 200, "remaining": 100, "unlimited": false } }, "quota_reset_date": "2025-02-01T00:00:00Z" }`, Template: " {{ .Premium.Percent }}% | {{ .Inline.Percent }}% | {{ .Chat.Percent }}% ", ExpectedString: "50% | 25% | 50%", ExpectedEnabled: true, HasToken: true, }, { Case: "No access token", ExpectedEnabled: false, HasError: false, }, { Case: "API error", HasError: true, ExpectedEnabled: false, HasToken: true, }, { Case: "Invalid JSON response", JSONResponse: "invalid json", ExpectedEnabled: false, HasToken: true, }, { Case: "Empty quota data", JSONResponse: `{}`, ExpectedEnabled: false, HasToken: true, }, { Case: "Null quota_snapshots", JSONResponse: `{"quota_snapshots": null}`, ExpectedEnabled: false, HasToken: true, }, } for _, tc := range cases { t.Run(tc.Case, func(t *testing.T) { env := &mock.Environment{} props := options.Map{} // Setup cached token mock if tc.HasToken { cache.Set(cache.Device, auth.CopilotTokenKey, "ghp_test_token", cache.INFINITE) } else { cache.Delete(cache.Device, auth.CopilotTokenKey) } // Setup HTTP request mock var httpErr error if tc.HasError { httpErr = errors.New("request failed") } if tc.HasToken { env.On("HTTPRequest", copilotTestURL).Return([]byte(tc.JSONResponse), httpErr) } c := &Copilot{} c.Init(props, env) enabled := c.Enabled() assert.Equal(t, tc.ExpectedEnabled, enabled, tc.Case) if !enabled { return } template := tc.Template if template == "" { template = c.Template() } assert.Equal(t, tc.ExpectedString, renderTemplate(env, template, c), tc.Case) }) } } func TestCopilotPercentageCalculation(t *testing.T) { cases := []struct { Case string Used int Limit int ExpectedPercent int }{ { Case: "50 percent", Used: 50, Limit: 100, ExpectedPercent: 50, }, { Case: "Zero limit", Used: 10, Limit: 0, ExpectedPercent: 0, }, { Case: "Negative limit", Used: 10, Limit: -5, ExpectedPercent: 0, }, { Case: "Over 100 percent caps at 100", Used: 150, Limit: 100, ExpectedPercent: 100, }, { Case: "Zero used", Used: 0, Limit: 100, ExpectedPercent: 0, }, { Case: "Full usage", Used: 100, Limit: 100, ExpectedPercent: 100, }, } for _, tc := range cases { t.Run(tc.Case, func(t *testing.T) { c := &Copilot{} result := c.calculatePercent(tc.Used, tc.Limit) assert.Equal(t, tc.ExpectedPercent, result, tc.Case) }) } } func TestCopilotRemainingPercentage(t *testing.T) { env := &mock.Environment{} props := options.Map{} jsonResponse := `{ "quota_snapshots": { "premium_interactions": { "entitlement": 100, "remaining": 75, "unlimited": false }, "completions": { "entitlement": 1000, "remaining": 600, "unlimited": false }, "chat": { "entitlement": 200, "remaining": 0, "unlimited": false } }, "quota_reset_date": "2025-02-15T00:00:00Z" }` cache.Set(cache.Device, auth.CopilotTokenKey, "ghp_test_token", cache.INFINITE) env.On("HTTPRequest", copilotTestURL).Return([]byte(jsonResponse), nil) c := &Copilot{} c.Init(props, env) enabled := c.Enabled() assert.True(t, enabled) // Test Premium: 100 entitlement - 75 remaining = 25 used (25% used, 75% remaining) assert.Equal(t, text.Percentage(25), c.Premium.Percent) assert.Equal(t, text.Percentage(75), c.Premium.Remaining) // Test Inline: 1000 entitlement - 600 remaining = 400 used (40% used, 60% remaining) assert.Equal(t, text.Percentage(40), c.Inline.Percent) assert.Equal(t, text.Percentage(60), c.Inline.Remaining) // Test Chat: 200 entitlement - 0 remaining = 200 used (100% used, 0% remaining) assert.Equal(t, text.Percentage(100), c.Chat.Percent) assert.Equal(t, text.Percentage(0), c.Chat.Remaining) } func TestCopilotBillingCycleEnd(t *testing.T) { env := &mock.Environment{} props := options.Map{} jsonResponse := `{ "quota_snapshots": { "premium_interactions": { "entitlement": 50, "remaining": 35, "unlimited": false }, "completions": { "entitlement": 2000, "remaining": 1500, "unlimited": false }, "chat": { "entitlement": 100, "remaining": 80, "unlimited": false } }, "quota_reset_date": "2025-02-15T00:00:00Z" }` cache.Set(cache.Device, auth.CopilotTokenKey, "ghp_test_token", cache.INFINITE) env.On("HTTPRequest", copilotTestURL).Return([]byte(jsonResponse), nil) c := &Copilot{} c.Init(props, env) enabled := c.Enabled() assert.True(t, enabled) assert.Equal(t, "2025-02-15T00:00:00Z", c.BillingCycleEnd) } ================================================ FILE: src/segments/crystal.go ================================================ package segments type Crystal struct { Language } func (c *Crystal) Template() string { return languageTemplate } func (c *Crystal) Enabled() bool { c.extensions = []string{"*.cr", "shard.yml"} c.tooling = map[string]*cmd{ "crystal": { executable: "crystal", args: []string{"--version"}, regex: `Crystal (?P((?P[0-9]+).(?P[0-9]+).(?P[0-9]+)))`, }, } c.defaultTooling = []string{"crystal"} c.versionURLTemplate = "https://github.com/crystal-lang/crystal/releases/tag/{{ .Full }}" return c.Language.Enabled() } ================================================ FILE: src/segments/crystal_test.go ================================================ package segments import ( "fmt" "testing" "github.com/stretchr/testify/assert" ) func TestCrystal(t *testing.T) { cases := []struct { Case string ExpectedString string Version string }{ {Case: "Crystal 1.0.0", ExpectedString: "1.0.0", Version: "Crystal 1.0.0 (2021-03-22)"}, } for _, tc := range cases { params := &mockedLanguageParams{ cmd: "crystal", versionParam: "--version", versionOutput: tc.Version, extension: "*.cr", } env, props := getMockedLanguageEnv(params) c := &Crystal{} c.Init(props, env) assert.True(t, c.Enabled(), fmt.Sprintf("Failed in case: %s", tc.Case)) assert.Equal(t, tc.ExpectedString, renderTemplate(env, c.Template(), c), fmt.Sprintf("Failed in case: %s", tc.Case)) } } ================================================ FILE: src/segments/dart.go ================================================ package segments var ( dartExtensions = []string{"*.dart", "pubspec.yaml", "pubspec.yml", "pubspec.lock"} dartFolders = []string{".dart_tool"} ) type Dart struct { Language } func (d *Dart) Template() string { return languageTemplate } func (d *Dart) Enabled() bool { d.extensions = dartExtensions d.folders = dartFolders d.tooling = map[string]*cmd{ "fvm": { executable: "fvm", args: []string{"dart", "--version"}, regex: `Dart SDK version: (?P((?P[0-9]+).(?P[0-9]+).(?P[0-9]+)))`, }, "dart": { executable: "dart", args: []string{"--version"}, regex: `Dart SDK version: (?P((?P[0-9]+).(?P[0-9]+).(?P[0-9]+)))`, }, } d.defaultTooling = []string{"fvm", "dart"} d.versionURLTemplate = "https://dart.dev/guides/language/evolution#dart-{{ .Major }}{{ .Minor }}" return d.Language.Enabled() } ================================================ FILE: src/segments/dart_test.go ================================================ package segments import ( "fmt" "testing" "github.com/stretchr/testify/assert" ) func TestDart(t *testing.T) { cases := []struct { Case string ExpectedString string Version string }{ {Case: "Dart 2.12.4", ExpectedString: "2.12.4", Version: "Dart SDK version: 2.12.4 (stable) (Thu Apr 15 12:26:53 2021 +0200) on \"macos_x64\""}, } for _, tc := range cases { params := &mockedLanguageParams{ cmd: "dart", versionParam: "--version", versionOutput: tc.Version, extension: "*.dart", } env, props := getMockedLanguageEnv(params) env.On("HasCommand", "fvm").Return(false) d := &Dart{} d.Init(props, env) assert.True(t, d.Enabled(), fmt.Sprintf("Failed in case: %s", tc.Case)) assert.Equal(t, tc.ExpectedString, renderTemplate(env, d.Template(), d), fmt.Sprintf("Failed in case: %s", tc.Case)) } } ================================================ FILE: src/segments/deno.go ================================================ package segments type Deno struct { Language } func (d *Deno) Template() string { return languageTemplate } func (d *Deno) Enabled() bool { d.extensions = []string{"*.js", "*.ts", "deno.json"} d.tooling = map[string]*cmd{ "deno": { executable: "deno", args: []string{"--version"}, regex: `(?:(?P((?P[0-9]+).(?P[0-9]+).(?P[0-9]+))))`, }, } d.defaultTooling = []string{"deno"} d.versionURLTemplate = "https://github.com/denoland/deno/releases/tag/v{{.Full}}" return d.Language.Enabled() } ================================================ FILE: src/segments/deno_test.go ================================================ package segments import ( "fmt" "testing" "github.com/stretchr/testify/assert" ) func TestDeno(t *testing.T) { cases := []struct { Case string ExpectedString string Version string }{ {Case: "Deno 1.25.2", ExpectedString: "1.25.2", Version: "deno 1.25.2 (release, aarch64-apple-darwin)\nv8 10.6.194.5\ntypescript 4.7.4"}, } for _, tc := range cases { params := &mockedLanguageParams{ cmd: "deno", versionParam: "--version", versionOutput: tc.Version, extension: "*.js", } env, props := getMockedLanguageEnv(params) d := &Deno{} d.Init(props, env) assert.True(t, d.Enabled(), fmt.Sprintf("Failed in case: %s", tc.Case)) assert.Equal(t, tc.ExpectedString, renderTemplate(env, d.Template(), d), fmt.Sprintf("Failed in case: %s", tc.Case)) } } ================================================ FILE: src/segments/docker.go ================================================ package segments import ( "encoding/json" "path/filepath" "slices" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" ) const ( // FetchContext is the property used to fetch the current docker context FetchContext options.Option = "fetch_context" ) type DockerConfig struct { CurrentContext string `json:"currentContext"` } type Docker struct { Base Context string } func (d *Docker) Template() string { return " \uf308 {{ .Context }} " } func (d *Docker) envVars() []string { return []string{"DOCKER_MACHINE_NAME", "DOCKER_HOST", "DOCKER_CONTEXT"} } func (d *Docker) configFiles() []string { files := []string{ filepath.Join(d.env.Home(), ".docker/config.json"), } dockerConfig := d.env.Getenv("DOCKER_CONFIG") if len(dockerConfig) > 0 { files = append(files, filepath.Join(dockerConfig, "config.json")) } return files } func (d *Docker) Enabled() bool { extensions := []string{ "compose.yml", "compose.yaml", "docker-compose.yml", "docker-compose.yaml", "Dockerfile", } extensions = d.options.StringArray(LanguageExtensions, extensions) displayMode := d.options.String(DisplayMode, DisplayModeContext) switch displayMode { case DisplayModeContext: return d.fetchContext() case DisplayModeFiles: if !slices.ContainsFunc(extensions, d.env.HasFiles) { return false } // always respect the context fetching if d.options.Bool(FetchContext, true) { _ = d.fetchContext() } return true } return false } func (d *Docker) fetchContext() bool { // Check if there is a non-empty environment variable named `DOCKER_HOST` or `DOCKER_CONTEXT` // These variables are set by the docker CLI and override the config file // Return the current context if it is not empty and not `default` for _, v := range d.envVars() { context := d.env.Getenv(v) if len(context) > 0 && context != "default" { d.Context = context return true } } // Check if there is a file named `$HOME/.docker/config.json` or `$DOCKER_CONFIG/config.json` // Return the current context if it is not empty and not `default` for _, f := range d.configFiles() { data := d.env.FileContent(f) if data == "" { continue } var cfg DockerConfig if err := json.Unmarshal([]byte(data), &cfg); err != nil { continue } if len(cfg.CurrentContext) > 0 && cfg.CurrentContext != "default" { d.Context = cfg.CurrentContext return true } } return false } ================================================ FILE: src/segments/docker_test.go ================================================ package segments import ( "testing" "github.com/jandedobbeleer/oh-my-posh/src/runtime/mock" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" "github.com/stretchr/testify/assert" mock_ "github.com/stretchr/testify/mock" ) func TestDockerContext(t *testing.T) { type envVar struct { name string value string } cases := []struct { EnvVar envVar Case string Expected string ConfigFile string ExpectedEnabled bool HasFiles bool }{ {Case: "DOCKER_MACHINE_NAME", Expected: "alpine", ExpectedEnabled: true, EnvVar: envVar{name: "DOCKER_MACHINE_NAME", value: "alpine"}}, {Case: "DOCKER_HOST", Expected: "alpine 2", ExpectedEnabled: true, EnvVar: envVar{name: "DOCKER_HOST", value: "alpine 2"}}, {Case: "DOCKER_CONTEXT", Expected: "alpine 3", ExpectedEnabled: true, EnvVar: envVar{name: "DOCKER_HOST", value: "alpine 3"}}, {Case: "DOCKER_CONTEXT - default", ExpectedEnabled: false, EnvVar: envVar{name: "DOCKER_HOST", value: "default"}}, {Case: "no docker context active", ExpectedEnabled: false}, {Case: "config file", Expected: "alpine", ExpectedEnabled: true, HasFiles: true, ConfigFile: `{"currentContext": "alpine"}`}, {Case: "config file - default", ExpectedEnabled: false, HasFiles: true, ConfigFile: `{"currentContext": "default"}`}, {Case: "config file - broken", ExpectedEnabled: false, HasFiles: true, ConfigFile: `{`}, } for _, tc := range cases { docker := &Docker{} env := new(mock.Environment) docker.Init(options.Map{}, env) for _, v := range docker.envVars() { var value string if v == tc.EnvVar.name { value = tc.EnvVar.value } env.On("Getenv", v).Return(value) } env.On("Home").Return("") env.On("Getenv", "DOCKER_CONFIG").Return("") for _, f := range docker.configFiles() { env.On("HasFiles", f).Return(tc.HasFiles) env.On("FileContent", f).Return(tc.ConfigFile) } assert.Equal(t, tc.ExpectedEnabled, docker.Enabled(), tc.Case) if tc.ExpectedEnabled { assert.Equal(t, tc.Expected, renderTemplate(env, "{{ .Context }}", docker), tc.Case) } } } func TestDockerFiles(t *testing.T) { cases := []struct { Case string ExpectedEnabled bool HasFiles bool }{ {Case: "compose.yml", ExpectedEnabled: true, HasFiles: true}, {Case: "compose.yaml", ExpectedEnabled: true, HasFiles: true}, {Case: "docker-compose.yml", ExpectedEnabled: true, HasFiles: true}, {Case: "docker-compose.yaml", ExpectedEnabled: true, HasFiles: true}, {Case: "Dockerfile", ExpectedEnabled: true, HasFiles: true}, {Case: "docker-compose.yml - not found", ExpectedEnabled: false, HasFiles: false}, } for _, tc := range cases { docker := &Docker{} env := new(mock.Environment) props := options.Map{ DisplayMode: DisplayModeFiles, FetchContext: false, } docker.Init(props, env) env.On("HasFiles", tc.Case).Return(true) env.On("HasFiles", mock_.Anything).Return(false) assert.Equal(t, tc.ExpectedEnabled, docker.Enabled(), tc.Case) } } ================================================ FILE: src/segments/dotnet.go ================================================ package segments import ( "encoding/json" "github.com/jandedobbeleer/oh-my-posh/src/constants" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" ) type globalJSON struct { Sdk struct { Version string `json:"version"` } `json:"sdk"` } const ( // FetchSDKVersion fetches the SDK version in global.json FetchSDKVersion options.Option = "fetch_sdk_version" ) type Dotnet struct { SDKVersion string Language Unsupported bool } func (d *Dotnet) Template() string { return " {{ if .Unsupported }}\uf071{{ else }}{{ .Full }}{{ end }} " } func (d *Dotnet) Enabled() bool { d.extensions = []string{ "*.cs", "*.csx", "*.vb", "*.sln", "*.slnx", "*.slnf", "*.csproj", "*.vbproj", "*.fs", "*.fsx", "*.fsproj", "global.json", } d.tooling = map[string]*cmd{ "dotnet": { executable: "dotnet", args: []string{"--version"}, regex: `(?P((?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)` + `(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?))`, }, } d.defaultTooling = []string{"dotnet"} d.versionURLTemplate = "https://github.com/dotnet/core/blob/main/release-notes/{{ .Major }}.{{ .Minor }}/{{ .Major }}.{{ .Minor }}.{{ substr 0 1 .Patch }}/{{ .Major }}.{{ .Minor }}.{{ substr 0 1 .Patch }}.md" //nolint: lll enabled := d.Language.Enabled() if !enabled { return false } d.Unsupported = d.exitCode == constants.DotnetExitCode if !d.options.Bool(FetchSDKVersion, false) { return true } file, err := d.env.HasParentFilePath("global.json", false) if err != nil { return true } content := d.env.FileContent(file.Path) var globalJSON globalJSON if err := json.Unmarshal([]byte(content), &globalJSON); err == nil { d.SDKVersion = globalJSON.Sdk.Version } return true } ================================================ FILE: src/segments/dotnet_test.go ================================================ package segments import ( "errors" "testing" "github.com/jandedobbeleer/oh-my-posh/src/constants" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" "github.com/stretchr/testify/assert" ) func TestDotnetSegment(t *testing.T) { cases := []struct { Case string Expected string Version string ExitCode int }{ {Case: "Unsupported version", Expected: "\uf071", ExitCode: constants.DotnetExitCode, Version: "3.1.402"}, {Case: "Regular version", Expected: "3.1.402", Version: "3.1.402"}, } for _, tc := range cases { params := &mockedLanguageParams{ cmd: "dotnet", versionParam: "--version", versionOutput: tc.Version, extension: "*.cs", } env, props := getMockedLanguageEnv(params) if tc.ExitCode != 0 { env.Unset("RunCommand") err := &runtime.CommandError{ExitCode: tc.ExitCode} env.On("RunCommand", "dotnet", []string{"--version"}).Return("", err) } dotnet := &Dotnet{} dotnet.Init(props, env) assert.True(t, dotnet.Enabled()) assert.Equal(t, tc.Expected, renderTemplate(env, dotnet.Template(), dotnet), tc.Case) } } func TestDotnetSDKVersion(t *testing.T) { cases := []struct { Case string GlobalJSON string ExpectedSDK string GlobalJSONPath string FetchSDK bool HasGlobalJSON bool }{ { Case: "Do not fetch SDK version", FetchSDK: false, ExpectedSDK: "", }, { Case: "No global.json found", FetchSDK: true, ExpectedSDK: "", }, { Case: "Valid global.json", FetchSDK: true, GlobalJSON: `{"sdk": {"version": "6.0.100"}}`, ExpectedSDK: "6.0.100", HasGlobalJSON: true, GlobalJSONPath: "/test/global.json", }, { Case: "Invalid global.json", FetchSDK: true, GlobalJSON: `invalid json`, ExpectedSDK: "", HasGlobalJSON: true, GlobalJSONPath: "/test/global.json", }, } params := &mockedLanguageParams{ cmd: "dotnet", versionParam: "--version", versionOutput: "6.0.100", extension: "*.cs", } for _, tc := range cases { props := options.Map{ FetchSDKVersion: tc.FetchSDK, options.FetchVersion: false, } env, _ := getMockedLanguageEnv(params) if tc.HasGlobalJSON { file := &runtime.FileInfo{ Path: tc.GlobalJSONPath, } env.On("HasParentFilePath", "global.json", false).Return(file, nil) env.On("FileContent", tc.GlobalJSONPath).Return(tc.GlobalJSON) } else { env.On("HasParentFilePath", "global.json", false).Return(&runtime.FileInfo{}, errors.New("file not found")) } dotnet := &Dotnet{} dotnet.Init(props, env) assert.True(t, dotnet.Enabled(), tc.Case) assert.Equal(t, tc.ExpectedSDK, dotnet.SDKVersion, tc.Case) } } ================================================ FILE: src/segments/elixir.go ================================================ package segments type Elixir struct { Language } func (e *Elixir) Template() string { return languageTemplate } func (e *Elixir) Enabled() bool { e.extensions = []string{"*.ex", "*.exs"} e.tooling = map[string]*cmd{ "asdf": { executable: "asdf", args: []string{"current", "elixir"}, regex: `elixir\s+(?P((?P[0-9]+).(?P[0-9]+).(?P[0-9]+)))[^\s]*\s+`, }, "elixir": { executable: "elixir", args: []string{"--version"}, regex: `Elixir (?P((?P[0-9]+).(?P[0-9]+).(?P[0-9]+)))`, }, } e.defaultTooling = []string{"asdf", "elixir"} e.versionURLTemplate = "https://github.com/elixir-lang/elixir/releases/tag/v{{ .Full }}" return e.Language.Enabled() } ================================================ FILE: src/segments/elixir_test.go ================================================ package segments import ( "fmt" "testing" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/stretchr/testify/assert" ) func TestElixir(t *testing.T) { cases := []struct { Case string ExpectedString string ElixirVersionOutput string AsdfVersionOutput string HasAsdf bool AsdfExitCode int }{ { Case: "Version without asdf", ExpectedString: "1.14.2", ElixirVersionOutput: "Erlang/OTP 25 [erts-13.1.3] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit] [dtrace]\n\nElixir 1.14.2 (compiled with Erlang/OTP 25)", }, { Case: "Version with asdf", ExpectedString: "1.14.2", HasAsdf: true, AsdfVersionOutput: "elixir 1.14.2-otp-25 /path/to/.tool-versions", ElixirVersionOutput: "Should not be used", }, { Case: "Version with asdf not set: should fall back to elixir --version", ExpectedString: "1.14.2", HasAsdf: true, AsdfVersionOutput: "elixir ______ No version is set. Run \"asdf elixir \"", AsdfExitCode: 126, ElixirVersionOutput: "Erlang/OTP 25 [erts-13.1.3] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit] [dtrace]\n\nElixir 1.14.2 (compiled with Erlang/OTP 25)", }, } for _, tc := range cases { params := &mockedLanguageParams{ cmd: "elixir", versionParam: "--version", versionOutput: tc.ElixirVersionOutput, extension: "*.ex", } env, props := getMockedLanguageEnv(params) env.On("HasCommand", "asdf").Return(tc.HasAsdf) var asdfErr error if tc.AsdfExitCode != 0 { asdfErr = &runtime.CommandError{ExitCode: tc.AsdfExitCode} } env.On("RunCommand", "asdf", []string{"current", "elixir"}).Return(tc.AsdfVersionOutput, asdfErr) r := &Elixir{} r.Init(props, env) assert.True(t, r.Enabled(), fmt.Sprintf("Failed in case: %s", tc.Case)) assert.Equal(t, tc.ExpectedString, renderTemplate(env, r.Template(), r), fmt.Sprintf("Failed in case: %s", tc.Case)) } } ================================================ FILE: src/segments/executiontime.go ================================================ package segments import ( "fmt" "strconv" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" lang "golang.org/x/text/language" "golang.org/x/text/message" ) type Executiontime struct { Base FormattedMs string Ms int64 } // DurationStyle how to display the time type DurationStyle string const ( // ThresholdProperty represents minimum duration (milliseconds) required to enable this segment ThresholdProperty options.Option = "threshold" // Austin milliseconds short Austin DurationStyle = "austin" // Roundrock milliseconds long Roundrock DurationStyle = "roundrock" // Dallas milliseconds full Dallas DurationStyle = "dallas" // Galveston hour Galveston DurationStyle = "galveston" // Galveston hour GalvestonMs DurationStyle = "galvestonms" // Houston hour and milliseconds Houston DurationStyle = "houston" // Amarillo seconds Amarillo DurationStyle = "amarillo" // Round will round the output of the format Round DurationStyle = "round" // Always 7 character width Lucky7 = "lucky7" // ISO8601 ISO 8601 duration format (seconds) ISO8601 DurationStyle = "iso8601" // ISO8601Ms ISO 8601 duration format with milliseconds ISO8601Ms DurationStyle = "iso8601ms" second = 1000 minute = 60000 hour = 3600000 day = 86400000 secondsPerMinute = 60 minutesPerHour = 60 hoursPerDay = 24 ) func (t *Executiontime) Enabled() bool { alwaysEnabled := t.options.Bool(options.AlwaysEnabled, false) executionTimeMs := t.env.ExecutionTime() thresholdMs := t.options.Float64(ThresholdProperty, float64(500)) if !alwaysEnabled && executionTimeMs < thresholdMs { return false } style := DurationStyle(t.options.String(options.Style, string(Austin))) t.Ms = int64(executionTimeMs) t.FormattedMs = t.formatDuration(style) return t.FormattedMs != "" } func (t *Executiontime) Template() string { return " {{ .FormattedMs }} " } func (t *Executiontime) formatDuration(style DurationStyle) string { switch style { case Austin: return t.formatDurationAustin() case Roundrock: return t.formatDurationRoundrock() case Dallas: return t.formatDurationDallas() case Galveston: return t.formatDurationGalveston() case GalvestonMs: return t.formatDurationGalvestonMs() case Houston: return t.formatDurationHouston() case Amarillo: return t.formatDurationAmarillo() case Round: return t.formatDurationRound() case Lucky7: return t.formatDurationLucky7() case ISO8601: return t.formatDurationISO8601() case ISO8601Ms: return t.formatDurationISO8601Ms() default: return fmt.Sprintf("Style: %s is not available", style) } } func (t *Executiontime) formatDurationAustin() string { if t.Ms < second { return fmt.Sprintf("%dms", t.Ms%second) } seconds := float64(t.Ms%minute) / second result := strconv.FormatFloat(seconds, 'f', -1, 64) + "s" if t.Ms >= minute { result = fmt.Sprintf("%dm %s", t.Ms/minute%secondsPerMinute, result) } if t.Ms >= hour { result = fmt.Sprintf("%dh %s", t.Ms/hour%hoursPerDay, result) } if t.Ms >= day { result = fmt.Sprintf("%dd %s", t.Ms/day, result) } return result } func (t *Executiontime) formatDurationRoundrock() string { result := fmt.Sprintf("%dms", t.Ms%second) if t.Ms >= second { result = fmt.Sprintf("%ds %s", t.Ms/second%secondsPerMinute, result) } if t.Ms >= minute { result = fmt.Sprintf("%dm %s", t.Ms/minute%minutesPerHour, result) } if t.Ms >= hour { result = fmt.Sprintf("%dh %s", t.Ms/hour%hoursPerDay, result) } if t.Ms >= day { result = fmt.Sprintf("%dd %s", t.Ms/day, result) } return result } func (t *Executiontime) formatDurationDallas() string { seconds := float64(t.Ms%minute) / second result := strconv.FormatFloat(seconds, 'f', -1, 64) if t.Ms >= minute { result = fmt.Sprintf("%d:%s", t.Ms/minute%minutesPerHour, result) } if t.Ms >= hour { result = fmt.Sprintf("%d:%s", t.Ms/hour%hoursPerDay, result) } if t.Ms >= day { result = fmt.Sprintf("%d:%s", t.Ms/day, result) } return result } func (t *Executiontime) formatDurationGalveston() string { result := fmt.Sprintf("%02d:%02d:%02d", t.Ms/hour, t.Ms/minute%minutesPerHour, t.Ms%minute/second) return result } func (t *Executiontime) formatDurationGalvestonMs() string { millies := t.Ms % second result := fmt.Sprintf("%02d:%02d:%02d:%03d", t.Ms/hour, t.Ms/minute%minutesPerHour, t.Ms%minute/second, millies) return result } func (t *Executiontime) formatDurationHouston() string { milliseconds := ".0" if t.Ms%second > 0 { // format milliseconds as a string with truncated trailing zeros milliseconds = strconv.FormatFloat(float64(t.Ms%second)/second, 'f', -1, 64) // at this point milliseconds looks like "0.5". remove the leading "0" if len(milliseconds) >= 1 { milliseconds = milliseconds[1:] } } result := fmt.Sprintf("%02d:%02d:%02d%s", t.Ms/hour, t.Ms/minute%minutesPerHour, t.Ms%minute/second, milliseconds) return result } func (t *Executiontime) formatDurationAmarillo() string { // wholeNumber represents the value to the left of the decimal point (seconds) wholeNumber := t.Ms / second // decimalNumber represents the value to the right of the decimal point (milliseconds) decimalNumber := float64(t.Ms%second) / second // format wholeNumber as a string with thousands separators printer := message.NewPrinter(lang.English) result := printer.Sprintf("%d", wholeNumber) if decimalNumber > 0 { // format decimalNumber as a string with truncated trailing zeros decimalResult := strconv.FormatFloat(decimalNumber, 'f', -1, 64) // at this point decimalResult looks like "0.5" // remove the leading "0" and append if len(decimalResult) >= 1 { result += decimalResult[1:] } } result += "s" return result } func (t *Executiontime) formatDurationRound() string { toRoundString := func(one, two int64, oneText, twoText string) string { if two == 0 { return fmt.Sprintf("%d%s", one, oneText) } return fmt.Sprintf("%d%s %d%s", one, oneText, two, twoText) } hours := t.Ms / hour % hoursPerDay if t.Ms >= day { return toRoundString(t.Ms/day, hours, "d", "h") } minutes := t.Ms / minute % secondsPerMinute if t.Ms >= hour { return toRoundString(hours, minutes, "h", "m") } seconds := (t.Ms % minute) / second if t.Ms >= minute { return toRoundString(minutes, seconds, "m", "s") } if t.Ms >= second { return fmt.Sprintf("%ds", seconds) } return fmt.Sprintf("%dms", t.Ms%second) } func (t *Executiontime) formatDurationLucky7() string { // https://github.com/JanDeDobbeleer/oh-my-posh/issues/3970 // execution time will always be 7 characters long // decimal point will be at the same location (3rd space or str[2]) // seconds and milliseconds will be aligned // [m, s], [h, m], [d, h] will be aligned if t.Ms < second { // 999ms // 1234567 return fmt.Sprintf("%5dms", t.Ms%second) } if t.Ms < minute { // 12.34s // 1234567 // 1.23s // 1230 (= 1230ms) // ^ use Sprintf pad left space // 1230 // from here, just take 1, 23 of 230, and append s and ' ' result := fmt.Sprintf("%5d", t.Ms) return result[:2] + "." + result[2:4] + "s " } if t.Ms < hour { m := t.Ms / minute s := t.Ms % minute / second return fmt.Sprintf("%2dm %2ds", m, s) } if t.Ms < day { h := t.Ms / hour m := t.Ms % hour / minute return fmt.Sprintf("%2dh %2dm", h, m) } if t.Ms < 100*day { d := t.Ms / day h := t.Ms % day / hour return fmt.Sprintf("%2dd %2dh", d, h) } // I have no Idea how you got here // return " ∞ " d := t.Ms / day return fmt.Sprintf("%6dd", d) } func (t *Executiontime) formatDurationISO8601() string { // ISO 8601 duration format: PT[n]H[n]M[n]S // Examples: PT13M12S, PT1H30M45S result := "PT" hours := t.Ms / hour minutes := (t.Ms % hour) / minute seconds := float64(t.Ms%minute) / second roundedSeconds := int64(seconds) if t.Ms%second >= second/2 { roundedSeconds++ } // Handle potential overflow from rounding if roundedSeconds >= secondsPerMinute { roundedSeconds = 0 minutes++ if minutes >= minutesPerHour { minutes = 0 hours++ } } if hours > 0 { result += fmt.Sprintf("%dH", hours) } if minutes > 0 { result += fmt.Sprintf("%dM", minutes) } if roundedSeconds > 0 || (hours == 0 && minutes == 0) { result += fmt.Sprintf("%dS", roundedSeconds) } return result } func (t *Executiontime) formatDurationISO8601Ms() string { // ISO 8601 duration format with milliseconds: PT[n]H[n]M[n]S // Examples: PT13M12.1S, PT1H30M45.123S result := "PT" hours := t.Ms / hour minutes := (t.Ms % hour) / minute seconds := float64(t.Ms%minute) / second if hours > 0 { result += fmt.Sprintf("%dH", hours) } if minutes > 0 { result += fmt.Sprintf("%dM", minutes) } if seconds > 0 || (hours == 0 && minutes == 0) { secondsStr := strconv.FormatFloat(seconds, 'f', -1, 64) result += fmt.Sprintf("%sS", secondsStr) } return result } ================================================ FILE: src/segments/executiontime_test.go ================================================ package segments import ( "math" "testing" "time" "github.com/jandedobbeleer/oh-my-posh/src/runtime/mock" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" "github.com/stretchr/testify/assert" ) func TestExecutionTimeWriterDefaultThresholdEnabled(t *testing.T) { env := new(mock.Environment) env.On("ExecutionTime").Return(1337) executionTime := &Executiontime{} executionTime.Init(options.Map{}, env) assert.True(t, executionTime.Enabled()) } func TestExecutionTimeWriterDefaultThresholdDisabled(t *testing.T) { env := new(mock.Environment) env.On("ExecutionTime").Return(1) executionTime := &Executiontime{} executionTime.Init(options.Map{}, env) assert.False(t, executionTime.Enabled()) } func TestExecutionTimeWriterCustomThresholdEnabled(t *testing.T) { env := new(mock.Environment) env.On("ExecutionTime").Return(99) props := options.Map{ ThresholdProperty: float64(10), } executionTime := &Executiontime{} executionTime.Init(props, env) assert.True(t, executionTime.Enabled()) } func TestExecutionTimeWriterCustomThresholdDisabled(t *testing.T) { env := new(mock.Environment) env.On("ExecutionTime").Return(99) props := options.Map{ ThresholdProperty: float64(100), } executionTime := &Executiontime{} executionTime.Init(props, env) assert.False(t, executionTime.Enabled()) } func TestExecutionTimeWriterDuration(t *testing.T) { input := 1337 expected := "1.337s" env := new(mock.Environment) env.On("ExecutionTime").Return(input) executionTime := &Executiontime{} executionTime.Init(options.Map{}, env) executionTime.Enabled() assert.Equal(t, expected, executionTime.FormattedMs) } func TestExecutionTimeWriterDuration2(t *testing.T) { input := 13371337 expected := "3h 42m 51.337s" env := new(mock.Environment) env.On("ExecutionTime").Return(input) executionTime := &Executiontime{} executionTime.Init(options.Map{}, env) executionTime.Enabled() assert.Equal(t, expected, executionTime.FormattedMs) } func TestExecutionTimeFormatDurationAustin(t *testing.T) { cases := []struct { Input string Expected string }{ {Input: "0.001s", Expected: "1ms"}, {Input: "0.1s", Expected: "100ms"}, {Input: "1s", Expected: "1s"}, {Input: "2.1s", Expected: "2.1s"}, {Input: "1m", Expected: "1m 0s"}, {Input: "3m2.1s", Expected: "3m 2.1s"}, {Input: "1h", Expected: "1h 0m 0s"}, {Input: "4h3m2.1s", Expected: "4h 3m 2.1s"}, {Input: "124h3m2.1s", Expected: "5d 4h 3m 2.1s"}, {Input: "124h3m2.0s", Expected: "5d 4h 3m 2s"}, } for _, tc := range cases { duration, _ := time.ParseDuration(tc.Input) executionTime := &Executiontime{} executionTime.Ms = duration.Milliseconds() output := executionTime.formatDurationAustin() assert.Equal(t, tc.Expected, output) } } func TestExecutionTimeFormatDurationRoundrock(t *testing.T) { cases := []struct { Input string Expected string }{ {Input: "0.001s", Expected: "1ms"}, {Input: "0.1s", Expected: "100ms"}, {Input: "1s", Expected: "1s 0ms"}, {Input: "2.1s", Expected: "2s 100ms"}, {Input: "1m", Expected: "1m 0s 0ms"}, {Input: "3m2.1s", Expected: "3m 2s 100ms"}, {Input: "1h", Expected: "1h 0m 0s 0ms"}, {Input: "4h3m2.1s", Expected: "4h 3m 2s 100ms"}, {Input: "124h3m2.1s", Expected: "5d 4h 3m 2s 100ms"}, {Input: "124h3m2.0s", Expected: "5d 4h 3m 2s 0ms"}, } for _, tc := range cases { duration, _ := time.ParseDuration(tc.Input) executionTime := &Executiontime{} executionTime.Ms = duration.Milliseconds() output := executionTime.formatDurationRoundrock() assert.Equal(t, tc.Expected, output) } } func TestExecutionTimeFormatDallas(t *testing.T) { cases := []struct { Input string Expected string }{ {Input: "0.001s", Expected: "0.001"}, {Input: "0.1s", Expected: "0.1"}, {Input: "1s", Expected: "1"}, {Input: "2.1s", Expected: "2.1"}, {Input: "1m", Expected: "1:0"}, {Input: "3m2.1s", Expected: "3:2.1"}, {Input: "1h", Expected: "1:0:0"}, {Input: "4h3m2.1s", Expected: "4:3:2.1"}, {Input: "124h3m2.1s", Expected: "5:4:3:2.1"}, {Input: "124h3m2.0s", Expected: "5:4:3:2"}, } for _, tc := range cases { duration, _ := time.ParseDuration(tc.Input) executionTime := &Executiontime{} executionTime.Ms = duration.Milliseconds() output := executionTime.formatDurationDallas() assert.Equal(t, tc.Expected, output) } } func TestExecutionTimeFormatGalveston(t *testing.T) { cases := []struct { Input string Expected string }{ {Input: "0.001s", Expected: "00:00:00"}, {Input: "0.1s", Expected: "00:00:00"}, {Input: "1s", Expected: "00:00:01"}, {Input: "2.1s", Expected: "00:00:02"}, {Input: "1m", Expected: "00:01:00"}, {Input: "3m2.1s", Expected: "00:03:02"}, {Input: "1h", Expected: "01:00:00"}, {Input: "4h3m2.1s", Expected: "04:03:02"}, {Input: "124h3m2.1s", Expected: "124:03:02"}, {Input: "124h3m2.0s", Expected: "124:03:02"}, } for _, tc := range cases { duration, _ := time.ParseDuration(tc.Input) executionTime := &Executiontime{} executionTime.Ms = duration.Milliseconds() output := executionTime.formatDurationGalveston() assert.Equal(t, tc.Expected, output) } } func TestExecutionTimeFormatGalvestonMs(t *testing.T) { cases := []struct { Input string Expected string }{ {Input: "0.001s", Expected: "00:00:00:001"}, {Input: "0.1s", Expected: "00:00:00:100"}, {Input: "1s", Expected: "00:00:01:000"}, {Input: "2.1s", Expected: "00:00:02:100"}, {Input: "1m", Expected: "00:01:00:000"}, {Input: "3m2.1s", Expected: "00:03:02:100"}, {Input: "1h", Expected: "01:00:00:000"}, {Input: "4h3m2.1s", Expected: "04:03:02:100"}, {Input: "124h3m2.1s", Expected: "124:03:02:100"}, {Input: "124h3m2.0s", Expected: "124:03:02:000"}, } for _, tc := range cases { duration, _ := time.ParseDuration(tc.Input) executionTime := &Executiontime{} executionTime.Ms = duration.Milliseconds() output := executionTime.formatDurationGalvestonMs() assert.Equal(t, tc.Expected, output, tc.Input) } } func TestExecutionTimeFormatHouston(t *testing.T) { cases := []struct { Input string Expected string }{ {Input: "0.001s", Expected: "00:00:00.001"}, {Input: "0.1s", Expected: "00:00:00.1"}, {Input: "1s", Expected: "00:00:01.0"}, {Input: "2.1s", Expected: "00:00:02.1"}, {Input: "1m", Expected: "00:01:00.0"}, {Input: "3m2.1s", Expected: "00:03:02.1"}, {Input: "1h", Expected: "01:00:00.0"}, {Input: "4h3m2.1s", Expected: "04:03:02.1"}, {Input: "124h3m2.1s", Expected: "124:03:02.1"}, {Input: "124h3m2.0s", Expected: "124:03:02.0"}, } for _, tc := range cases { duration, _ := time.ParseDuration(tc.Input) executionTime := &Executiontime{} executionTime.Ms = duration.Milliseconds() output := executionTime.formatDurationHouston() assert.Equal(t, tc.Expected, output) } } func TestExecutionTimeFormatAmarillo(t *testing.T) { cases := []struct { Input string Expected string }{ {Input: "0.001s", Expected: "0.001s"}, {Input: "0.1s", Expected: "0.1s"}, {Input: "1s", Expected: "1s"}, {Input: "2.1s", Expected: "2.1s"}, {Input: "1m", Expected: "60s"}, {Input: "3m2.1s", Expected: "182.1s"}, {Input: "1h", Expected: "3,600s"}, {Input: "4h3m2.1s", Expected: "14,582.1s"}, {Input: "124h3m2.1s", Expected: "446,582.1s"}, {Input: "124h3m2.0s", Expected: "446,582s"}, } for _, tc := range cases { duration, _ := time.ParseDuration(tc.Input) executionTime := &Executiontime{} executionTime.Ms = duration.Milliseconds() output := executionTime.formatDurationAmarillo() assert.Equal(t, tc.Expected, output) } } func TestExecutionTimeFormatDurationRound(t *testing.T) { cases := []struct { Input string Expected string }{ {Input: "0.001s", Expected: "1ms"}, {Input: "0.1s", Expected: "100ms"}, {Input: "1s", Expected: "1s"}, {Input: "2.1s", Expected: "2s"}, {Input: "1m", Expected: "1m"}, {Input: "3m2.1s", Expected: "3m 2s"}, {Input: "1h", Expected: "1h"}, {Input: "4h3m2.1s", Expected: "4h 3m"}, {Input: "124h3m2.1s", Expected: "5d 4h"}, {Input: "124h3m2.0s", Expected: "5d 4h"}, } for _, tc := range cases { duration, _ := time.ParseDuration(tc.Input) executionTime := &Executiontime{} executionTime.Ms = duration.Milliseconds() output := executionTime.formatDurationRound() assert.Equal(t, tc.Expected, output) } } func TestExecutionTimeFormatDurationLucky7(t *testing.T) { cases := []struct { Input string Expected string }{ { Input: "0.001s", Expected: " 1ms", }, { Input: "0.1s", Expected: " 100ms", }, { Input: "1s", Expected: " 1.00s ", }, { Input: "2.1s", Expected: " 2.10s ", }, { Input: "1m", Expected: " 1m 0s", }, { Input: "3m2.1s", Expected: " 3m 2s", }, { Input: "1h", Expected: " 1h 0m", }, { Input: "4h3m2.1s", Expected: " 4h 3m", }, { Input: "124h3m2.1s", Expected: " 5d 4h", }, { Input: "124h3m2.0s", Expected: " 5d 4h", }, } for _, tc := range cases { duration, _ := time.ParseDuration(tc.Input) executionTime := &Executiontime{} executionTime.Ms = duration.Milliseconds() output := executionTime.formatDurationLucky7() assert.Equal(t, tc.Expected, output) } // Extra fuzz test var timestamp int64 = 1 var ms1000days int64 = 1000 * 24 * 60 * 60 * 1000 // log(ms1000days, 1.5) is approx 62.1 for timestamp < ms1000days { timestamp = int64(math.Ceil(float64(timestamp) * 1.5)) executionTime := (&Executiontime{ Ms: timestamp, }).formatDurationLucky7() // Lucky 7!! assert.Equal(t, len(executionTime), 7) } } func TestExecutionTimeFormatISO8601(t *testing.T) { cases := []struct { Input string Expected string }{ {Input: "0.001s", Expected: "PT0S"}, {Input: "0.1s", Expected: "PT0S"}, {Input: "0.5s", Expected: "PT1S"}, {Input: "1s", Expected: "PT1S"}, {Input: "2.1s", Expected: "PT2S"}, {Input: "2.6s", Expected: "PT3S"}, {Input: "1m", Expected: "PT1M"}, {Input: "3m2.1s", Expected: "PT3M2S"}, {Input: "3m2.6s", Expected: "PT3M3S"}, {Input: "1h", Expected: "PT1H"}, {Input: "4h3m2.1s", Expected: "PT4H3M2S"}, {Input: "124h3m2.1s", Expected: "PT124H3M2S"}, {Input: "124h3m2.0s", Expected: "PT124H3M2S"}, } for _, tc := range cases { duration, _ := time.ParseDuration(tc.Input) executionTime := &Executiontime{} executionTime.Ms = duration.Milliseconds() output := executionTime.formatDurationISO8601() assert.Equal(t, tc.Expected, output, "Input: %s", tc.Input) } } func TestExecutionTimeFormatISO8601Ms(t *testing.T) { cases := []struct { Input string Expected string }{ {Input: "0.001s", Expected: "PT0.001S"}, {Input: "0.1s", Expected: "PT0.1S"}, {Input: "1s", Expected: "PT1S"}, {Input: "2.1s", Expected: "PT2.1S"}, {Input: "2.123s", Expected: "PT2.123S"}, {Input: "1m", Expected: "PT1M"}, {Input: "3m2.1s", Expected: "PT3M2.1S"}, {Input: "3m2.123s", Expected: "PT3M2.123S"}, {Input: "1h", Expected: "PT1H"}, {Input: "4h3m2.1s", Expected: "PT4H3M2.1S"}, {Input: "124h3m2.123s", Expected: "PT124H3M2.123S"}, } for _, tc := range cases { duration, _ := time.ParseDuration(tc.Input) executionTime := &Executiontime{} executionTime.Ms = duration.Milliseconds() output := executionTime.formatDurationISO8601Ms() assert.Equal(t, tc.Expected, output, "Input: %s", tc.Input) } } ================================================ FILE: src/segments/firebase.go ================================================ package segments import ( "encoding/json" "errors" "path/filepath" "strings" "github.com/jandedobbeleer/oh-my-posh/src/log" ) const ( FIREBASENOACTIVECONFIG = "NO ACTIVE CONFIG FOUND" ) type Firebase struct { Base Project string } type FirebaseData struct { ActiveProject map[string]string `json:"activeProjects"` } func (f *Firebase) Template() string { return " {{ .Project}} " } func (f *Firebase) Enabled() bool { cfgDir := filepath.Join(f.env.Home(), ".config", "configstore") configFile, err := f.getActiveConfig(cfgDir) if err != nil { log.Error(err) return false } data, err := f.getFirebaseData(configFile) if err != nil { log.Error(err) return false } // Within the activeProjects is a key value pair // of the path to the project and the project name // Test if the current directory is a project path // and if it is, return the project name for key, value := range data.ActiveProject { if strings.HasPrefix(f.env.Pwd(), key) { f.Project = value return true } } return false } func (f *Firebase) getActiveConfig(cfgDir string) (string, error) { activeConfigFile := filepath.Join(cfgDir, "firebase-tools.json") activeConfigData := f.env.FileContent(activeConfigFile) if activeConfigData == "" { return "", errors.New(FIREBASENOACTIVECONFIG) } return activeConfigData, nil } func (f *Firebase) getFirebaseData(configFile string) (*FirebaseData, error) { var data FirebaseData err := json.Unmarshal([]byte(configFile), &data) if err != nil { return nil, err } return &data, nil } ================================================ FILE: src/segments/firebase_test.go ================================================ package segments import ( "path/filepath" "testing" "github.com/jandedobbeleer/oh-my-posh/src/runtime/mock" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" "github.com/stretchr/testify/assert" ) func TestFirebaseSegment(t *testing.T) { config := `{ "activeProjects": { "path": "project-name" } }` cases := []struct { Case string ActiveConfig string ActivePath string ExpectedString string ExpectedEnabled bool }{ { Case: "happy path", ExpectedEnabled: true, ActiveConfig: config, ActivePath: "path", ExpectedString: "project-name", }, { Case: "happy subpath", ExpectedEnabled: true, ActiveConfig: config, ActivePath: "path/subpath", ExpectedString: "project-name", }, { Case: "no active config", ExpectedEnabled: false, }, { Case: "empty config", ActiveConfig: "{}", ExpectedEnabled: false, }, { Case: "bad config", ActiveConfig: "{bad}", ExpectedEnabled: false, }, } for _, tc := range cases { env := new(mock.Environment) env.On("Home").Return("home") env.On("Pwd").Return(tc.ActivePath) fcPath := filepath.Join("home", ".config", "configstore", "firebase-tools.json") env.On("FileContent", fcPath).Return(tc.ActiveConfig) f := &Firebase{} f.Init(options.Map{}, env) f.Enabled() assert.Equal(t, tc.ExpectedEnabled, f.Enabled()) if tc.ExpectedEnabled { assert.Equal(t, tc.ExpectedString, renderTemplate(env, f.Template(), f), tc.Case) } } } func TestGetFirebaseActiveConfig(t *testing.T) { data := `{ "activeProjects": { "path": "project-name" } }` cases := []struct { Case string ActiveConfig string ExpectedString string ExpectedError string }{ { Case: "happy path", ActiveConfig: data, ExpectedString: data, }, { Case: "no active config", ActiveConfig: "", ExpectedError: FIREBASENOACTIVECONFIG, }, } for _, tc := range cases { env := new(mock.Environment) env.On("Home").Return("home") configPath := filepath.Join("home", ".config", "configstore") contentPath := filepath.Join(configPath, "firebase-tools.json") env.On("FileContent", contentPath).Return(tc.ActiveConfig) f := &Firebase{} f.Init(options.Map{}, env) got, err := f.getActiveConfig(configPath) assert.Equal(t, tc.ExpectedString, got, tc.Case) if len(tc.ExpectedError) > 0 { assert.EqualError(t, err, tc.ExpectedError, tc.Case) } else { assert.NoError(t, err, tc.Case) } } } ================================================ FILE: src/segments/flutter.go ================================================ package segments type Flutter struct { Language } func (f *Flutter) Template() string { return languageTemplate } func (f *Flutter) Enabled() bool { f.extensions = dartExtensions f.folders = dartFolders f.tooling = map[string]*cmd{ "fvm": { executable: "fvm", args: []string{"flutter", "--version"}, regex: `Flutter (?P((?P[0-9]+).(?P[0-9]+).(?P[0-9]+)))`, }, "flutter": { executable: "flutter", args: []string{"--version"}, regex: `Flutter (?P((?P[0-9]+).(?P[0-9]+).(?P[0-9]+)))`, }, } f.defaultTooling = []string{"fvm", "flutter"} f.versionURLTemplate = "https://github.com/flutter/flutter/releases/tag/{{ .Major }}.{{ .Minor }}.{{ .Patch }}" return f.Language.Enabled() } ================================================ FILE: src/segments/flutter_test.go ================================================ package segments import ( "fmt" "testing" "github.com/stretchr/testify/assert" ) func TestFlutter(t *testing.T) { cases := []struct { Case string ExpectedString string Version string }{ {Case: "Flutter 2.10.4", ExpectedString: "2.10.4", Version: "Flutter 2.10.4 • channel stable • https://github.com/flutter/flutter.git"}, } for _, tc := range cases { params := &mockedLanguageParams{ cmd: "flutter", versionParam: "--version", versionOutput: tc.Version, extension: "*.dart", } env, props := getMockedLanguageEnv(params) env.On("HasCommand", "fvm").Return(false) d := &Flutter{} d.Init(props, env) assert.True(t, d.Enabled(), fmt.Sprintf("Failed in case: %s", tc.Case)) assert.Equal(t, tc.ExpectedString, renderTemplate(env, d.Template(), d), fmt.Sprintf("Failed in case: %s", tc.Case)) } } ================================================ FILE: src/segments/fortran.go ================================================ package segments type Fortran struct { Language } func (f *Fortran) Template() string { return languageTemplate } func (f *Fortran) Enabled() bool { f.extensions = []string{ "*.f", "*.for", "*.fpp", "*.f77", "*.f90", "*.f95", "*.f03", "*.f08", "*.F", "*.FOR", "*.FPP", "*.F77", "*.F90", "*.F95", "*.F03", "*.F08", "fpm.toml", } f.tooling = map[string]*cmd{ "gfortran": { executable: "gfortran", args: []string{"--version"}, regex: `GNU Fortran \(.*\) (?P((?P[0-9]+).(?P[0-9]+).(?P[0-9]+)))`, }, } f.defaultTooling = []string{"gfortran"} return f.Language.Enabled() } ================================================ FILE: src/segments/fortran_test.go ================================================ package segments import ( "fmt" "testing" "github.com/stretchr/testify/assert" ) func TestFortran(t *testing.T) { cases := []struct { Case string ExpectedString string Version string }{ { Case: "GNU Fortran 10.2.1 Debian", ExpectedString: "10.2.1", Version: `GNU Fortran (Debian 10.2.1-6) 10.2.1 20210110 Copyright (C) 2020 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.`, }, { Case: "GNU Fortran 11.4.0 Ubuntu", ExpectedString: "11.4.0", Version: `GNU Fortran (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0 Copyright (C) 2021 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.`, }, } for _, tc := range cases { params := &mockedLanguageParams{ cmd: "gfortran", versionParam: "--version", versionOutput: tc.Version, extension: "*.f", } env, props := getMockedLanguageEnv(params) f := &Fortran{} f.Init(props, env) assert.True(t, f.Enabled(), fmt.Sprintf("Failed in case: %s", tc.Case)) assert.Equal(t, tc.ExpectedString, renderTemplate(env, f.Template(), f), fmt.Sprintf("Failed in case: %s", tc.Case)) } } ================================================ FILE: src/segments/fossil.go ================================================ package segments import "strings" // FossilStatus represents part of the status of a Svn repository type FossilStatus struct { ScmStatus } func (s *FossilStatus) add(code string) { switch code { case "CONFLICT": s.Conflicted++ case "DELETED", "MISSING": s.Deleted++ case "ADDED", "ADDED_BY_INTEGRATE", "ADDED_BY_MERGE": s.Added++ case "EDITED", "UPDATED", "UPDATED_BY_INTEGRATE", "UPDATED_BY_MERGE", "CHANGED": s.Modified++ case "RENAMED": s.Moved++ } } const ( FOSSILCOMMAND = "fossil" ) type Fossil struct { Status *FossilStatus Branch string Scm } func (f *Fossil) Template() string { return " \ue725 {{.Branch}} {{.Status.String}} " } func (f *Fossil) Enabled() bool { if !f.hasCommand(FOSSILCOMMAND) { return false } // run fossil command output, err := f.env.RunCommand(f.command, "status") if err != nil { return false } f.Status = &FossilStatus{} lines := strings.SplitSeq(output, "\n") for line := range lines { key, value, found := strings.Cut(line, " ") if !found { continue } switch key { case "tags:": f.Branch = strings.TrimSpace(value) default: f.Status.add(key) } } return true } ================================================ FILE: src/segments/fossil_test.go ================================================ package segments import ( "fmt" "strings" "testing" "github.com/jandedobbeleer/oh-my-posh/src/runtime/mock" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" "github.com/stretchr/testify/assert" ) func TestFossilStatus(t *testing.T) { cases := []struct { OutputError error Case string Output string ExpectedStatus string ExpectedBranch string HasCommand bool ExpectedDisabled bool }{ { Case: "not installed", HasCommand: false, ExpectedDisabled: true, }, { Case: "command error", HasCommand: true, OutputError: fmt.Errorf("error"), ExpectedDisabled: true, }, { Case: "default status", HasCommand: true, Output: ` repository: /Users/jan/Downloads/myclone.fossil local-root: /Users/jan/Projects/fossil/ config-db: /Users/jan/.config/fossil.db checkout: 0fabc4f3566c7e7d9e528b17253de42e14dd5c7b 2022-06-05 04:06:17 UTC parent: e8a051e6a943a26c9c33a30df8ceda069c06c174 2022-06-04 23:09:02 UTC tags: trunk comment: In the /setup_skin page, add a mention of/link to /skins, per request in the forum. (user: stephan) CONFLICT test.tst DELETED test.tst MISSING test.tst ADDED test.tst ADDED_BY_INTEGRATE test.tst ADDED_BY_MERGE test.tst EDITED auto.def UPDATED test.tst UPDATED_BY_INTEGRATE test.tst UPDATED_BY_MERGE test.tst CHANGED test.tst RENAMED test.tst `, ExpectedBranch: "trunk", ExpectedStatus: "+3 ~5 -2 >1 !1", }, } for _, tc := range cases { env := new(mock.Environment) env.On("GOOS").Return("unix") env.On("IsWsl").Return(false) env.On("InWSLSharedDrive").Return(false) env.On("HasCommand", FOSSILCOMMAND).Return(tc.HasCommand) env.On("RunCommand", FOSSILCOMMAND, []string{"status"}).Return(strings.ReplaceAll(tc.Output, "\t", ""), tc.OutputError) f := &Fossil{} f.Init(options.Map{}, env) got := f.Enabled() assert.Equal(t, !tc.ExpectedDisabled, got, tc.Case) if tc.ExpectedDisabled { continue } assert.Equal(t, tc.ExpectedStatus, f.Status.String(), tc.Case) assert.Equal(t, tc.ExpectedBranch, f.Branch, tc.Case) } } ================================================ FILE: src/segments/gcp.go ================================================ package segments import ( "errors" "path" "github.com/jandedobbeleer/oh-my-posh/src/log" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "gopkg.in/ini.v1" ) const ( GCPNOACTIVECONFIG = "NO ACTIVE CONFIG FOUND" ) type Gcp struct { Base Account string Project string Region string ActiveConfig string } func (g *Gcp) Template() string { return " {{ .Project }} " } func (g *Gcp) Enabled() bool { cfgDir := g.getConfigDirectory() cfgName, err := g.getActiveConfig(cfgDir) if err != nil { log.Error(err) return false } g.ActiveConfig = cfgName cfgPath := path.Join(cfgDir, "configurations", "config_"+cfgName) cfg := g.env.FileContent(cfgPath) if cfg == "" { log.Error(errors.New("config file is empty")) return false } data, err := ini.Load([]byte(cfg)) if err != nil { log.Error(err) return false } g.Project = data.Section("core").Key("project").String() g.Account = data.Section("core").Key("account").String() g.Region = data.Section("compute").Key("region").String() return true } func (g *Gcp) getActiveConfig(cfgDir string) (string, error) { activeCfg := g.env.Getenv("CLOUDSDK_ACTIVE_CONFIG_NAME") if len(activeCfg) != 0 { return activeCfg, nil } ap := path.Join(cfgDir, "active_config") activeCfg = g.env.FileContent(ap) if activeCfg == "" { return "", errors.New(GCPNOACTIVECONFIG) } return activeCfg, nil } func (g *Gcp) getConfigDirectory() string { cfgDir := g.env.Getenv("CLOUDSDK_CONFIG") if len(cfgDir) != 0 { return cfgDir } if g.env.GOOS() == runtime.WINDOWS { return path.Join(g.env.Getenv("APPDATA"), "gcloud") } return path.Join(g.env.Home(), ".config", "gcloud") } ================================================ FILE: src/segments/gcp_test.go ================================================ package segments import ( "path" "testing" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/runtime/mock" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" "github.com/stretchr/testify/assert" ) func TestGcpSegment(t *testing.T) { cases := []struct { Case string CfgData string ActiveConfig string EnvActiveConfig string ExpectedString string ExpectedEnabled bool }{ { Case: "happy path", ExpectedEnabled: true, ActiveConfig: "production", CfgData: ` [core] account = test@example.com project = test-test-test [compute] region = europe-test1 `, ExpectedString: "test-test-test :: europe-test1 :: test@example.com", }, { Case: "no active config", ExpectedEnabled: false, }, { Case: "empty config", ActiveConfig: "production", ExpectedEnabled: false, }, { Case: "bad config", ActiveConfig: "production", CfgData: "{bad}", ExpectedEnabled: false, }, { Case: "use CLOUDSDK_ACTIVE_CONFIG_NAME", EnvActiveConfig: "myconfig", ExpectedEnabled: true, CfgData: ` [core] account = user@example.com project = cloud-proj [compute] region = us-west1 `, ExpectedString: "cloud-proj :: us-west1 :: user@example.com", }, } for _, tc := range cases { env := new(mock.Environment) env.On("Getenv", "CLOUDSDK_CONFIG").Return("config") env.On("Getenv", "CLOUDSDK_ACTIVE_CONFIG_NAME").Return(tc.EnvActiveConfig) // Only use fallback file if env var is not set if tc.EnvActiveConfig == "" { fcPath := path.Join("config", "active_config") env.On("FileContent", fcPath).Return(tc.ActiveConfig) } // Resolve active config name activeConfig := tc.EnvActiveConfig if activeConfig == "" { activeConfig = tc.ActiveConfig } cfgpath := path.Join("config", "configurations", "config_"+activeConfig) env.On("FileContent", cfgpath).Return(tc.CfgData) g := &Gcp{} g.Init(options.Map{}, env) assert.Equal(t, tc.ExpectedEnabled, g.Enabled(), tc.Case) if tc.ExpectedEnabled { assert.Equal(t, tc.ExpectedString, renderTemplate(env, "{{.Project}} :: {{.Region}} :: {{.Account}}", g), tc.Case) } } } func TestGetConfigDirectory(t *testing.T) { cases := []struct { Case string GOOS string Home string AppData string CloudSDKConfig string Expected string }{ { Case: "CLOUDSDK_CONFIG", CloudSDKConfig: "/Users/posh/.config/gcloud", Expected: "/Users/posh/.config/gcloud", }, { Case: "Windows", GOOS: runtime.WINDOWS, AppData: "/Users/posh/.config", Expected: "/Users/posh/.config/gcloud", }, { Case: "default", Home: "/Users/posh2/", Expected: "/Users/posh2/.config/gcloud", }, } for _, tc := range cases { env := new(mock.Environment) env.On("Getenv", "CLOUDSDK_CONFIG").Return(tc.CloudSDKConfig) env.On("Getenv", "APPDATA").Return(tc.AppData) env.On("Home").Return(tc.Home) env.On("GOOS").Return(tc.GOOS) g := &Gcp{} g.Init(options.Map{}, env) assert.Equal(t, tc.Expected, g.getConfigDirectory(), tc.Case) } } func TestGetActiveConfig(t *testing.T) { cases := []struct { Case string EnvActiveConfigName string FileActiveConfigContent string ExpectedString string ExpectedError string }{ { Case: "CLOUDSDK_ACTIVE_CONFIG_NAME set", EnvActiveConfigName: "envconfig", ExpectedString: "envconfig", }, { Case: "Fallback to file content", FileActiveConfigContent: "fileconfig", ExpectedString: "fileconfig", }, { Case: "No config anywhere", ExpectedError: GCPNOACTIVECONFIG, }, } for _, tc := range cases { env := new(mock.Environment) env.On("Getenv", "CLOUDSDK_ACTIVE_CONFIG_NAME").Return(tc.EnvActiveConfigName) // If env var not set, mock file fallback if tc.EnvActiveConfigName == "" { env.On("FileContent", path.Join("", "active_config")).Return(tc.FileActiveConfigContent) } g := &Gcp{} g.Init(options.Map{}, env) got, err := g.getActiveConfig("") assert.Equal(t, tc.ExpectedString, got, tc.Case) if len(tc.ExpectedError) > 0 { assert.EqualError(t, err, tc.ExpectedError, tc.Case) } else { assert.NoError(t, err, tc.Case) } } } ================================================ FILE: src/segments/git.go ================================================ package segments import ( "fmt" url2 "net/url" "path/filepath" "strconv" "strings" "sync" "time" "github.com/jandedobbeleer/oh-my-posh/src/log" "github.com/jandedobbeleer/oh-my-posh/src/regex" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/runtime/path" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" "gopkg.in/ini.v1" ) type Commit struct { Timestamp time.Time Author *User Committer *User Refs *Refs Subject string Sha string } type Refs struct { Heads []string Tags []string Remotes []string } type User struct { Name string Email string } // GitStatus represents part of the status of a git repository type GitStatus struct { ScmStatus } func (s *GitStatus) add(code string) { switch code { case ".": return case "D": s.Deleted++ case "A": s.Added++ case "?": s.Untracked++ case "U", "AA": s.Unmerged++ case "M", "R", "C", "m": s.Modified++ } } const ( // FetchStatus fetches the status of the repository FetchStatus options.Option = "fetch_status" // FetchPushStatus fetches the push-remote status FetchPushStatus options.Option = "fetch_push_status" // IgnoreStatus allows to ignore certain repo's for status information IgnoreStatus options.Option = "ignore_status" // FetchUpstreamIcon fetches the upstream icon FetchUpstreamIcon options.Option = "fetch_upstream_icon" // FetchBareInfo fetches the bare repo status FetchBareInfo options.Option = "fetch_bare_info" // FetchUser fetches the current user for the repo FetchUser options.Option = "fetch_user" // UntrackedModes list the optional untracked files mode per repo UntrackedModes options.Option = "untracked_modes" // IgnoreSubmodules list the optional ignore-submodules mode per repo IgnoreSubmodules options.Option = "ignore_submodules" // MappedBranches allows overriding certain branches with an icon/text MappedBranches options.Option = "mapped_branches" // DisableWithJJ disables the git segment when there's a .jj directory in the parent file path DisableWithJJ options.Option = "disable_with_jj" // BranchIcon the icon to use as branch indicator BranchIcon options.Option = "branch_icon" // BranchIdenticalIcon the icon to display when the remote and local branch are identical BranchIdenticalIcon options.Option = "branch_identical_icon" // BranchAheadIcon the icon to display when the local branch is ahead of the remote BranchAheadIcon options.Option = "branch_ahead_icon" // BranchBehindIcon the icon to display when the local branch is behind the remote BranchBehindIcon options.Option = "branch_behind_icon" // BranchGoneIcon the icon to use when ther's no remote BranchGoneIcon options.Option = "branch_gone_icon" // RebaseIcon shows before the rebase context RebaseIcon options.Option = "rebase_icon" // CherryPickIcon shows before the cherry-pick context CherryPickIcon options.Option = "cherry_pick_icon" // RevertIcon shows before the revert context RevertIcon options.Option = "revert_icon" // CommitIcon shows before the detached context CommitIcon options.Option = "commit_icon" // NoCommitsIcon shows when there are no commits in the repo yet NoCommitsIcon options.Option = "no_commits_icon" // TagIcon shows before the tag context TagIcon options.Option = "tag_icon" // MergeIcon shows before the merge context MergeIcon options.Option = "merge_icon" // UpstreamIcons allows to add custom upstream icons UpstreamIcons options.Option = "upstream_icons" // GithubIcon shows when upstream is github GithubIcon options.Option = "github_icon" // BitbucketIcon shows when upstream is bitbucket BitbucketIcon options.Option = "bitbucket_icon" // AzureDevOpsIcon shows when upstream is azure devops AzureDevOpsIcon options.Option = "azure_devops_icon" // CodeCommit shows when upstream is aws codecommit CodeCommit options.Option = "codecommit_icon" // CodebergIcon shows when upstream is codeberg CodebergIcon options.Option = "codeberg_icon" // GitlabIcon shows when upstream is gitlab GitlabIcon options.Option = "gitlab_icon" // GitIcon shows when the upstream can't be identified GitIcon options.Option = "git_icon" DETACHED = "(detached)" BRANCHPREFIX = "ref: refs/heads/" GITCOMMAND = "git" trueStr = "true" origin = "origin" ) type Rebase struct { HEAD string Onto string Current int Total int } type Git struct { configErr error config *ini.File Working *GitStatus Staging *GitStatus commit *Commit Rebase *Rebase User *User ShortHash string Hash string BranchStatus string HEAD string UpstreamIcon string UpstreamURL string Ref string RawUpstreamURL string Scm stashCount int Ahead int PushAhead int PushBehind int Behind int worktreeCount int configOnce sync.Once IsWorkTree bool Merge bool CherryPick bool Revert bool poshgit bool Detached bool IsBare bool UpstreamGone bool } func (g *Git) Template() string { return " {{ .HEAD }}{{if .BranchStatus }} {{ .BranchStatus }}{{ end }}{{ if .Working.Changed }} \uF044 {{ .Working.String }}{{ end }}{{ if and (.Staging.Changed) (.Working.Changed) }} |{{ end }}{{ if .Staging.Changed }} \uF046 {{ .Staging.String }}{{ end }} " //nolint: lll } func (g *Git) Enabled() bool { g.User = &User{} g.Working = &GitStatus{} g.Staging = &GitStatus{} if !g.shouldDisplay() { return false } fetchUser := g.options.Bool(FetchUser, false) if fetchUser { g.setUser() } g.RepoName = g.repoName() if g.IsBare { g.getBareRepoInfo() return true } source := g.options.String(Source, Cli) if source == Pwsh && g.hasPoshGitStatus() { return true } displayStatus := g.options.Bool(FetchStatus, false) if displayStatus && g.shouldIgnoreStatus() { displayStatus = false } if displayStatus { g.setStatus() g.setHEADStatus() g.setBranchStatus() g.setPushStatus() } else { g.updateHEADReference() } if g.options.Bool(FetchUpstreamIcon, false) { g.UpstreamIcon = g.getUpstreamIcon() } return true } func (g *Git) CacheKey() (string, bool) { dir, err := g.env.HasParentFilePath(".git", true) if err != nil { return "", false } if !g.isRepo(dir) { return "", false } ref := g.fileContent(g.mainSCMDir, "HEAD") ref = strings.Replace(ref, "ref: refs/heads/", "", 1) // Use the repo clone in the cache key so the mapped path is consistent // for primary and worktree repos. return fmt.Sprintf("%s@%s", dir.Path, ref), true } func (g *Git) Commit() *Commit { if g.commit != nil { return g.commit } g.commit = &Commit{ Author: &User{}, Committer: &User{}, Refs: &Refs{}, } commitBody := g.getGitCommandOutput("log", "-1", "--pretty=format:an:%an%nae:%ae%ncn:%cn%nce:%ce%nat:%at%nsu:%s%nha:%H%nrf:%D", "--decorate=full") splitted := strings.SplitSeq(strings.TrimSpace(commitBody), "\n") for line := range splitted { line = strings.TrimSpace(line) if len(line) <= 3 { continue } anchor := line[:3] line = line[3:] switch anchor { case "an:": g.commit.Author.Name = line case "ae:": g.commit.Author.Email = line case "cn:": g.commit.Committer.Name = line case "ce:": g.commit.Committer.Email = line case "at:": if t, err := strconv.ParseInt(line, 10, 64); err == nil { g.commit.Timestamp = time.Unix(t, 0) } case "su:": g.commit.Subject = line case "ha:": g.commit.Sha = line case "rf:": refs := strings.SplitSeq(line, ", ") for ref := range refs { ref = strings.TrimSpace(ref) switch { case strings.HasSuffix(ref, "HEAD"): continue case strings.HasPrefix(ref, "tag: refs/tags/"): g.commit.Refs.Tags = append(g.commit.Refs.Tags, strings.TrimPrefix(ref, "tag: refs/tags/")) case strings.HasPrefix(ref, "refs/remotes/"): g.commit.Refs.Remotes = append(g.commit.Refs.Remotes, strings.TrimPrefix(ref, "refs/remotes/")) case strings.HasPrefix(ref, "HEAD -> refs/heads/"): g.commit.Refs.Heads = append(g.commit.Refs.Heads, strings.TrimPrefix(ref, "HEAD -> refs/heads/")) case strings.HasPrefix(ref, "refs/heads/"): g.commit.Refs.Heads = append(g.commit.Refs.Heads, strings.TrimPrefix(ref, "refs/heads/")) default: g.commit.Refs.Heads = append(g.commit.Refs.Heads, ref) } } } } return g.commit } func (g *Git) StashCount() int { if g.poshgit || g.stashCount != 0 { return g.stashCount } stashContent := g.fileContent(g.scmDir, "logs/refs/stash") if stashContent == "" { return 0 } g.stashCount = strings.Count(stashContent, "\n") + 1 // +1: fileContent() trims return g.stashCount } func (g *Git) Kraken() string { root := g.getGitCommandOutput("rev-list", "--max-parents=0", "HEAD") root, _, _ = strings.Cut(root, "\n") if g.RawUpstreamURL == "" { if g.Upstream == "" { g.Upstream = origin } g.RawUpstreamURL = g.getRemoteURL() } if g.Hash == "" { g.Hash = g.getGitCommandOutput("rev-parse", "HEAD") } return fmt.Sprintf("gitkraken://repolink/%s/commit/%s?url=%s", root, g.Hash, url2.QueryEscape(g.RawUpstreamURL)) } func (g *Git) LatestTag() string { return g.getGitCommandOutput("describe", "--tags", "--abbrev=0") } func (g *Git) shouldDisplay() bool { // Check if disable_with_jj is enabled and .jj directory exists if g.options.Bool(DisableWithJJ, false) { if _, err := g.env.HasParentFilePath(".jj", false); err == nil { return false } } gitdir, err := g.env.HasParentFilePath(".git", true) if err != nil { return false } if g.options.Bool(FetchBareInfo, false) { g.IsBare = g.isBareRepo(gitdir) } if !g.hasCommand(GITCOMMAND) { return false } return g.isRepo(gitdir) } func (g *Git) isRepo(gitdir *runtime.FileInfo) bool { g.setDir(gitdir.Path) if !gitdir.IsDir { if g.hasWorktree(gitdir) { g.repoRootDir = g.convertToWindowsPath(g.repoRootDir) return true } return false } g.mainSCMDir = gitdir.Path g.scmDir = gitdir.Path // convert the worktree file path to a windows one when in a WSL shared folder g.repoRootDir = strings.TrimSuffix(g.convertToWindowsPath(gitdir.Path), "/.git") return true } func (g *Git) setUser() { g.User.Name = g.getGitCommandOutput("config", "user.name") g.User.Email = g.getGitCommandOutput("config", "user.email") } func (g *Git) isBareRepo(gitDir *runtime.FileInfo) bool { defer log.Trace(time.Now()) if gitDir.IsDir { g.mainSCMDir = gitDir.Path } else { content := g.fileContent(gitDir.ParentFolder, ".git") dir := strings.TrimPrefix(content, "gitdir: ") g.mainSCMDir = filepath.Join(gitDir.ParentFolder, dir) } cfg, err := g.getGitConfig() if err != nil { log.Error(err) return false } coreSection := cfg.Section("core") if coreSection == nil { log.Debug("Git core section not found, not a bare repo") return false } bare := coreSection.Key("bare").String() return bare == trueStr } func (g *Git) getBareRepoInfo() { head := g.fileContent(g.mainSCMDir, "HEAD") branchIcon := g.options.String(BranchIcon, "\uE0A0") g.Ref = strings.Replace(head, "ref: refs/heads/", "", 1) g.HEAD = fmt.Sprintf("%s%s", branchIcon, g.formatBranch(g.Ref)) if !g.options.Bool(FetchUpstreamIcon, false) { return } g.Upstream = g.getGitCommandOutput("remote") if len(g.Upstream) != 0 { g.UpstreamIcon = g.getUpstreamIcon() } } func (g *Git) setDir(dir string) { dir = path.ReplaceHomeDirPrefixWithTilde(dir) // align with template PWD if g.env.GOOS() == runtime.WINDOWS { g.Dir = strings.TrimSuffix(dir, `\.git`) return } g.Dir = strings.TrimSuffix(dir, "/.git") } func (g *Git) hasWorktree(gitdir *runtime.FileInfo) bool { g.scmDir = gitdir.Path content := g.env.FileContent(gitdir.Path) content = strings.Trim(content, " \r\n") matches := regex.FindNamedRegexMatch(`^gitdir: (?P.*)$`, content) if len(matches) == 0 { log.Debug("no matches found, directory isn't a worktree") return false } // if we open a worktree file in a WSL shared folder, we have to convert it back // to the mounted path g.mainSCMDir = g.convertToLinuxPath(matches["dir"]) // in worktrees, the path looks like this: gitdir: path/.git/worktrees/branch // scmDir needs to become path/.git // repoRootDir needs to become path worktreeIndex := strings.LastIndex(g.mainSCMDir, "/worktrees/") // in submodules, the path looks like this: gitdir: ../.git/modules/test-submodule // we need the parent folder to detect where the real .git folder is if strings.Contains(g.mainSCMDir, "/modules/") { g.scmDir = resolveGitPath(gitdir.ParentFolder, g.mainSCMDir) // this might be both a worktree and a submodule, where the path would look like // this: path/.git/modules/module/path/worktrees/location. We cannot distinguish // between worktree and a module path containing the word 'worktree,' however. worktreeIndex = strings.LastIndex(g.scmDir, "/worktrees/") if worktreeIndex > -1 && g.env.HasFilesInDir(g.scmDir, "gitdir") { gitDir := filepath.Join(g.scmDir, "gitdir") realGitFolder := g.env.FileContent(gitDir) g.repoRootDir = strings.TrimSuffix(realGitFolder, ".git\n") // resolve relative paths (worktree.useRelativePaths = true) g.repoRootDir = resolveGitPath(g.scmDir, g.repoRootDir) g.scmDir = g.scmDir[:worktreeIndex] g.mainSCMDir = g.scmDir g.IsWorkTree = true return true } g.repoRootDir = g.scmDir g.mainSCMDir = g.scmDir return true } // convert to absolute path for worktrees only if strings.HasPrefix(g.mainSCMDir, "..") { g.mainSCMDir = filepath.Join(gitdir.ParentFolder, g.mainSCMDir) } if worktreeIndex > -1 { gitDir := filepath.Join(g.mainSCMDir, "gitdir") g.scmDir = g.mainSCMDir[:worktreeIndex] gitDirContent := g.env.FileContent(gitDir) g.repoRootDir = strings.TrimSuffix(gitDirContent, ".git\n") // resolve relative paths (worktree.useRelativePaths = true) g.repoRootDir = resolveGitPath(g.mainSCMDir, g.repoRootDir) g.IsWorkTree = true return true } // check for separate git folder(--separate-git-dir) // check if the folder contains a HEAD file if g.env.HasFilesInDir(g.mainSCMDir, "HEAD") { gitFolder := strings.TrimSuffix(g.scmDir, ".git") g.scmDir = g.mainSCMDir g.mainSCMDir = gitFolder g.repoRootDir = gitFolder return true } return false } func (g *Git) shouldIgnoreStatus() bool { list := g.options.StringArray(IgnoreStatus, []string{}) return g.env.DirMatchesOneOf(g.repoRootDir, list) } func (g *Git) setBranchStatus() { getBranchStatus := func() string { if g.Ahead > 0 && g.Behind > 0 { return fmt.Sprintf("%s%d %s%d", g.options.String(BranchAheadIcon, "\u2191"), g.Ahead, g.options.String(BranchBehindIcon, "\u2193"), g.Behind) } if g.Ahead > 0 { return fmt.Sprintf("%s%d", g.options.String(BranchAheadIcon, "\u2191"), g.Ahead) } if g.Behind > 0 { return fmt.Sprintf("%s%d", g.options.String(BranchBehindIcon, "\u2193"), g.Behind) } if g.UpstreamGone { return g.options.String(BranchGoneIcon, "\u2262") } if g.Behind == 0 && g.Ahead == 0 && g.Upstream != "" { return g.options.String(BranchIdenticalIcon, "\u2261") } return "" } g.BranchStatus = getBranchStatus() } func (g *Git) setPushStatus() { if !g.options.Bool(FetchPushStatus, false) { return } if g.Ref == "" || g.Ref == DETACHED { return } pushRemote := g.getPushRemote() if pushRemote == "" { return } ahead := g.getGitCommandOutput("rev-list", "--count", pushRemote+"..HEAD") if ahead != "" { g.PushAhead, _ = strconv.Atoi(strings.TrimSpace(ahead)) } behind := g.getGitCommandOutput("rev-list", "--count", "HEAD.."+pushRemote) if behind != "" { g.PushBehind, _ = strconv.Atoi(strings.TrimSpace(behind)) } } func (g *Git) getPushRemote() string { upstream := g.Upstream if idx := strings.Index(upstream, "/"); idx != -1 { upstream = upstream[:idx] } if upstream == "" { upstream = origin } branch := g.Ref if branch == "" { return "" } cfg, err := g.getGitConfig() if err != nil { pushRemote := g.getGitCommandOutput("config", "--get", "remote.pushDefault") if pushRemote == "" { pushRemote = upstream } return strings.TrimSpace(pushRemote) + "/" + branch } sectionName := fmt.Sprintf(`branch "%s"`, branch) section := cfg.Section(sectionName) pushRemote := section.Key("pushRemote").String() if pushRemote == "" { pushRemote = cfg.Section("remote").Key("pushDefault").String() } if pushRemote == "" { pushRemote = upstream } return pushRemote + "/" + branch } func (g *Git) getGitConfig() (*ini.File, error) { g.configOnce.Do(func() { configData := g.fileContent(g.mainSCMDir, "config") if configData == "" { log.Debug("git config file not found") g.configErr = fmt.Errorf("git config file not found") return } // ini.Load expects []byte to parse content, not a file path cfg, err := ini.Load([]byte(configData)) if err != nil { g.configErr = err return } g.config = cfg }) return g.config, g.configErr } func (g *Git) cleanUpstreamURL(url string) string { // Azure DevOps if strings.Contains(url, "dev.azure.com") { match := regex.FindNamedRegexMatch(`^.*@(ssh.)?dev\.azure\.com(:v3)?/(?P[A-Za-z0-9_-]+)/(?P[A-Za-z0-9_-]+)/(_git/)?(?P[A-Za-z0-9_-]+)$`, url) if len(match) == 4 { return fmt.Sprintf("https://dev.azure.com/%s/%s/_git/%s", match["ORGANIZATION"], match["PROJECT"], match["REPOSITORY"]) } } if strings.HasPrefix(url, "http") { return url } // /path/to/repo.git/ match := regex.FindNamedRegexMatch(`^(?P[a-z0-9./]+)$`, url) if len(match) != 0 { url := strings.Trim(match["URL"], "/") url = strings.TrimSuffix(url, ".git") return fmt.Sprintf("https://%s", strings.TrimPrefix(url, "/")) } // ssh://user@host.xz:1234/path/to/repo.git/ match = regex.FindNamedRegexMatch(`(ssh|ftp|git|rsync)://(.*@)?(?P[a-z0-9.-]+)(:[0-9]{1,5})?/(?P.*).git`, url) if len(match) == 0 { // host.xz:/path/to/repo.git/ match = regex.FindNamedRegexMatch(`^(?P[a-z0-9.-]+):(?P[\w.\-~/@]+)$`, url) } if len(match) != 0 { repoPath := strings.Trim(match["PATH"], "/") repoPath = strings.TrimSuffix(repoPath, ".git") return fmt.Sprintf("https://%s/%s", match["URL"], repoPath) } // codecommit::region-identifier-id://repo-name match = regex.FindNamedRegexMatch(`codecommit::(?P[a-z0-9-]+)://(?P[\w\.@\:/\-~]+)`, url) if len(match) != 0 { return fmt.Sprintf("https://%s.console.aws.amazon.com/codesuite/codecommit/repositories/%s/browse?region=%s", match["URL"], match["PATH"], match["URL"]) } // user@host.xz:/path/to/repo.git match = regex.FindNamedRegexMatch(`.*@(?P.*):(?P.*)`, url) if len(match) == 0 { return "" } return fmt.Sprintf("https://%s/%s", match["URL"], strings.TrimSuffix(match["PATH"], ".git")) } func (g *Git) getUpstreamIcon() string { fallback := g.options.String(GitIcon, "\uE5FB ") g.RawUpstreamURL = g.getRemoteURL() if g.RawUpstreamURL == "" { return fallback } g.UpstreamURL = g.cleanUpstreamURL(g.RawUpstreamURL) // allow overrides first custom := g.options.KeyValueMap(UpstreamIcons, map[string]string{}) for key, value := range custom { if strings.Contains(g.UpstreamURL, key) { return value } } defaults := map[string]struct { Icon options.Option Default string }{ "github": {GithubIcon, "\uF408"}, "gitlab": {GitlabIcon, "\uF296"}, "bitbucket": {BitbucketIcon, "\uF171"}, "dev.azure.com": {AzureDevOpsIcon, "\uEBE8"}, "visualstudio.com": {AzureDevOpsIcon, "\uEBE8"}, "codecommit": {CodeCommit, "\uF270"}, "codeberg": {CodebergIcon, "\uF330"}, } for key, value := range defaults { if strings.Contains(g.UpstreamURL, key) { return g.options.String(value.Icon, value.Default) } } return fallback } func (g *Git) setStatus() { addToStatus := func(status string) { const UNTRACKED = "?" if strings.HasPrefix(status, UNTRACKED) { g.Working.add(UNTRACKED) return } if len(status) <= 4 { return } // map conflicts separately when in a merge or rebase if g.Rebase != nil || g.Merge { conflict := "AA" full := status[2:4] if full == conflict { g.Staging.add(conflict) return } } workingCode := status[3:4] stagingCode := status[2:3] g.Working.add(workingCode) g.Staging.add(stagingCode) // A newly staged file (A.) exists in the working directory but hasn't been committed yet. // Reflect it in Working so that templates tracking only Working can see the new file. if stagingCode == "A" && workingCode == "." { g.Working.add("A") } } const ( HASH = "# branch.oid " REF = "# branch.head " UPSTREAM = "# branch.upstream " BRANCHSTATUS = "# branch.ab " ) // firstly assume that upstream is gone g.UpstreamGone = true statusFormats := g.options.KeyValueMap(StatusFormats, map[string]string{}) g.Working = &GitStatus{ScmStatus: ScmStatus{Formats: statusFormats}} g.Staging = &GitStatus{ScmStatus: ScmStatus{Formats: statusFormats}} untrackedMode := g.getUntrackedFilesMode() args := []string{"status", untrackedMode, "--branch", "--porcelain=2"} ignoreSubmodulesMode := g.getIgnoreSubmodulesMode() if len(ignoreSubmodulesMode) > 0 { args = append(args, ignoreSubmodulesMode) } output := g.getGitCommandOutput(args...) for line := range strings.SplitSeq(output, "\n") { if strings.HasPrefix(line, HASH) && len(line) >= len(HASH)+7 { g.ShortHash = line[len(HASH) : len(HASH)+7] g.Hash = line[len(HASH):] continue } if strings.HasPrefix(line, REF) && len(line) > len(REF) { g.Ref = line[len(REF):] continue } if strings.HasPrefix(line, UPSTREAM) && len(line) > len(UPSTREAM) { // status reports upstream, but upstream may be gone (must check BRANCHSTATUS) g.Upstream = line[len(UPSTREAM):] g.UpstreamGone = true continue } if strings.HasPrefix(line, BRANCHSTATUS) && len(line) > len(BRANCHSTATUS) { status := line[len(BRANCHSTATUS):] splitted := strings.SplitN(status, " ", 3) if len(splitted) >= 2 { g.Ahead, _ = strconv.Atoi(splitted[0]) behind, _ := strconv.Atoi(splitted[1]) g.Behind = -behind } // confirmed: upstream exists g.UpstreamGone = false continue } addToStatus(line) } } func (g *Git) getGitCommandOutput(args ...string) string { if g.command == "" { return "" } args = append([]string{"-C", g.repoRootDir, "--no-optional-locks", "-c", "core.quotepath=false", "-c", "color.status=false"}, args...) val, err := g.env.RunCommand(g.command, args...) if err != nil { return "" } return val } func (g *Git) setHEADStatus() { branchIcon := g.options.String(BranchIcon, "\uE0A0") if g.Ref == DETACHED { g.Detached = true g.resolveDetachedHEAD() } else { head := g.formatBranch(g.Ref) g.HEAD = fmt.Sprintf("%s%s", branchIcon, head) } formatDetached := func() string { if g.Detached { return fmt.Sprintf("%sdetached at %s", branchIcon, g.HEAD) } return g.HEAD } getPrettyNameOrigin := func(file string) string { var origin string head := g.fileContent(g.mainSCMDir, file) if head == "detached HEAD" { origin = formatDetached() } else { head = strings.Replace(head, "refs/heads/", "", 1) origin = branchIcon + g.formatBranch(head) } return origin } parseInt := func(file string) int { val, _ := strconv.Atoi(g.fileContent(g.mainSCMDir, file)) return val } if g.env.HasFolder(g.mainSCMDir + "/rebase-merge") { head := getPrettyNameOrigin("rebase-merge/head-name") onto := g.getGitRefFileSymbolicName("rebase-merge/onto") onto = g.formatBranch(onto) current := parseInt("rebase-merge/msgnum") total := parseInt("rebase-merge/end") icon := g.options.String(RebaseIcon, "\uE728 ") g.Rebase = &Rebase{ HEAD: head, Onto: onto, Current: current, Total: total, } g.HEAD = fmt.Sprintf("%s%s onto %s%s (%d/%d) at %s", icon, head, branchIcon, onto, current, total, g.HEAD) return } if g.env.HasFolder(g.mainSCMDir + "/rebase-apply") { head := getPrettyNameOrigin("rebase-apply/head-name") current := parseInt("rebase-apply/next") total := parseInt("rebase-apply/last") icon := g.options.String(RebaseIcon, "\uE728 ") g.Rebase = &Rebase{ HEAD: head, Current: current, Total: total, } g.HEAD = fmt.Sprintf("%s%s (%d/%d) at %s", icon, head, current, total, g.HEAD) return } // merge commitIcon := g.options.String(CommitIcon, "\uF417") if g.hasGitFile("MERGE_MSG") { g.Merge = true icon := g.options.String(MergeIcon, "\uE727 ") mergeContext := g.fileContent(g.mainSCMDir, "MERGE_MSG") matches := regex.FindNamedRegexMatch(`Merge (remote-tracking )?(?Pbranch|commit|tag) '(?P.*)'`, mergeContext) // head := g.getGitRefFileSymbolicName("ORIG_HEAD") if matches != nil && matches["theirs"] != "" { var headIcon, theirs string switch matches["type"] { case "tag": headIcon = g.options.String(TagIcon, "\uF412") theirs = matches["theirs"] case "commit": headIcon = commitIcon theirs = g.formatSHA(matches["theirs"]) default: headIcon = branchIcon theirs = g.formatBranch(matches["theirs"]) } g.HEAD = fmt.Sprintf("%s%s%s into %s", icon, headIcon, theirs, formatDetached()) return } } // sequencer status // see if a cherry-pick or revert is in progress, if the user has committed a // conflict resolution with 'git commit' in the middle of a sequence of picks or // reverts then CHERRY_PICK_HEAD/REVERT_HEAD will not exist so we have to read // the todo file. if g.hasGitFile("CHERRY_PICK_HEAD") { g.CherryPick = true sha := g.fileContent(g.mainSCMDir, "CHERRY_PICK_HEAD") cherry := g.options.String(CherryPickIcon, "\uE29B ") g.HEAD = fmt.Sprintf("%s%s%s onto %s", cherry, commitIcon, g.formatSHA(sha), formatDetached()) return } if g.hasGitFile("REVERT_HEAD") { g.Revert = true sha := g.fileContent(g.mainSCMDir, "REVERT_HEAD") revert := g.options.String(RevertIcon, "\uF0E2 ") g.HEAD = fmt.Sprintf("%s%s%s onto %s", revert, commitIcon, g.formatSHA(sha), formatDetached()) return } if g.hasGitFile("sequencer/todo") { todo := g.fileContent(g.mainSCMDir, "sequencer/todo") matches := regex.FindNamedRegexMatch(`^(?Pp|pick|revert)\s+(?P\S+)`, todo) if matches != nil && matches["sha"] != "" { action := matches["action"] sha := matches["sha"] switch action { case "p", "pick": g.CherryPick = true cherry := g.options.String(CherryPickIcon, "\uE29B ") g.HEAD = fmt.Sprintf("%s%s%s onto %s", cherry, commitIcon, g.formatSHA(sha), formatDetached()) return case "revert": g.Revert = true revert := g.options.String(RevertIcon, "\uF0E2 ") g.HEAD = fmt.Sprintf("%s%s%s onto %s", revert, commitIcon, g.formatSHA(sha), formatDetached()) return } } } g.HEAD = formatDetached() } func (g *Git) formatSHA(sha string) string { if len(sha) <= 7 { return sha } return sha[0:7] } func (g *Git) hasGitFile(file string) bool { return g.env.HasFilesInDir(g.mainSCMDir, file) } func (g *Git) getGitRefFileSymbolicName(refFile string) string { ref := g.fileContent(g.mainSCMDir, refFile) return g.getGitCommandOutput("name-rev", "--name-only", "--exclude=tags/*", ref) } func (g *Git) updateHEADReference() { HEADRef := g.fileContent(g.mainSCMDir, "HEAD") log.Debug("HEADRef:", HEADRef) // check if we are in a repo using reftables if HEADRef == "ref: refs/heads/.invalid" { log.Debug("repo is using reftables") HEADRef = g.getGitCommandOutput("rev-parse", "--symbolic-full-name", "HEAD") // this is a detached head if strings.HasPrefix(HEADRef, "fatal:") { log.Debug("detached HEAD detected") g.Detached = true g.resolveDetachedHEAD() return } if strings.HasPrefix(HEADRef, "refs/heads/") { HEADRef = "ref: " + HEADRef } log.Debug("resolved HEADRef:", HEADRef) } g.Detached = !strings.HasPrefix(HEADRef, "ref:") if branchName, ok := strings.CutPrefix(HEADRef, BRANCHPREFIX); ok { log.Debug("current HEAD is a branch:", branchName) g.Ref = branchName g.HEAD = fmt.Sprintf("%s%s", g.options.String(BranchIcon, "\uE0A0"), g.formatBranch(branchName)) return } g.resolveDetachedHEAD() } func (g *Git) resolveDetachedHEAD() { HEADRef := g.getGitCommandOutput("rev-parse", "HEAD") if len(HEADRef) >= 7 { g.ShortHash = HEADRef[0:7] g.Hash = HEADRef[0:] } g.Ref = g.ShortHash // check for tag tagName := g.getGitCommandOutput("describe", "--tags", "--exact-match") if len(tagName) > 0 { g.Ref = tagName g.HEAD = fmt.Sprintf("%s%s", g.options.String(TagIcon, "\uF412"), tagName) return } // fallback to no commits found if g.ShortHash == "" { g.HEAD = g.options.String(NoCommitsIcon, "\uF594 ") return } g.HEAD = fmt.Sprintf("%s%s", g.options.String(CommitIcon, "\uF417"), g.ShortHash) } func (g *Git) WorktreeCount() int { if g.worktreeCount > 0 { return g.worktreeCount } worktreesFolder := filepath.Join(g.mainSCMDir, "worktrees") if !g.env.HasFolder(worktreesFolder) { return 0 } worktreeFolders := g.env.LsDir(worktreesFolder) var count int for _, folder := range worktreeFolders { if folder.IsDir() { count++ } } return count } func (g *Git) getRemoteURL() string { upstream := regex.ReplaceAllString("/.*", g.Upstream, "") if upstream == "" { upstream = origin } cfg, err := g.getGitConfig() if err != nil { return g.getGitCommandOutput("remote", "get-url", upstream) } url := cfg.Section("remote \"" + upstream + "\"").Key("url").String() if len(url) != 0 { log.Debug("remote url found in config:", url) return url } return g.getGitCommandOutput("remote", "get-url", upstream) } func (g *Git) Remotes() map[string]string { var remotes = make(map[string]string) cfg, err := g.getGitConfig() if err != nil { return remotes } for _, section := range cfg.Sections() { if !strings.HasPrefix(section.Name(), "remote ") { continue } name := strings.TrimPrefix(section.Name(), "remote ") name = strings.Trim(name, "\"") url := section.Key("url").String() url = g.cleanUpstreamURL(url) remotes[name] = url } return remotes } func (g *Git) getUntrackedFilesMode() string { return g.getSwitchMode(UntrackedModes, "-u", "normal") } func (g *Git) getIgnoreSubmodulesMode() string { return g.getSwitchMode(IgnoreSubmodules, "--ignore-submodules=", "") } func (g *Git) getSwitchMode(property options.Option, gitSwitch, mode string) string { repoModes := g.options.KeyValueMap(property, map[string]string{}) // make use of a wildcard for all repo's if val := repoModes["*"]; len(val) != 0 { mode = val } // get the specific repo mode if val := repoModes[g.repoRootDir]; len(val) != 0 { mode = val } if mode == "" { return "" } return fmt.Sprintf("%s%s", gitSwitch, mode) } func (g *Git) repoName() string { if !g.IsWorkTree { return path.Base(g.convertToLinuxPath(g.repoRootDir)) } ind := strings.LastIndex(g.mainSCMDir, ".git/worktrees") if ind > -1 { return path.Base(g.mainSCMDir[:ind]) } return "" } ================================================ FILE: src/segments/git_test.go ================================================ package segments import ( "errors" "fmt" "os" "path/filepath" "strconv" "strings" "sync" "testing" "time" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/runtime/mock" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" "gopkg.in/ini.v1" "github.com/stretchr/testify/assert" testify_ "github.com/stretchr/testify/mock" ) const ( branchName = "main" dotGit = "dev/.git" dotGitSubmodule = "dev/.git/modules/submodule" ) func TestEnabledGitNotFound(t *testing.T) { env := new(mock.Environment) env.On("InWSLSharedDrive").Return(false) env.On("HasParentFilePath", ".git", true).Return((*runtime.FileInfo)(nil), errors.New("no .git found (mock)")) env.On("GOOS").Return("") env.On("IsWsl").Return(false) g := &Git{} g.Init(options.Map{}, env) assert.False(t, g.Enabled()) } func TestEnabledInWorkingDirectory(t *testing.T) { fileInfo := &runtime.FileInfo{ Path: "/dir/hello", ParentFolder: "/dir", IsDir: true, } env := new(mock.Environment) env.On("InWSLSharedDrive").Return(false) env.On("HasCommand", "git").Return(true) env.On("GOOS").Return("") env.On("FileContent", "/dir/hello/HEAD").Return("") env.MockGitCommand(fileInfo.Path, "1234567890abcdef1234567890abcdef12345678", "rev-parse", "HEAD") env.MockGitCommand(fileInfo.Path, "", "describe", "--tags", "--exact-match") env.On("IsWsl").Return(false) env.On("HasParentFilePath", ".git", true).Return(fileInfo, nil) env.On("PathSeparator").Return("/") env.On("Home").Return(poshHome) env.On("Getenv", poshGitEnv).Return("") env.On("DirMatchesOneOf", testify_.Anything, testify_.Anything).Return(false) g := &Git{} g.Init(options.Map{}, env) assert.True(t, g.Enabled()) assert.Equal(t, fileInfo.Path, g.mainSCMDir) } func TestResolveEmptyGitPath(t *testing.T) { base := "base" assert.Equal(t, base, resolveGitPath(base, "")) } func TestEnabledInWorktree(t *testing.T) { cases := []struct { Case string WorkingFolder string WorkingFolderAddon string WorkingFolderContent string ExpectedRealFolder string ExpectedWorkingFolder string ExpectedRootFolder string ExpectedEnabled bool }{ { Case: "worktree", ExpectedEnabled: true, WorkingFolder: TestRootPath + "dev/.git/worktrees/folder_worktree", WorkingFolderAddon: "gitdir", WorkingFolderContent: TestRootPath + "dev/worktree.git\n", ExpectedWorkingFolder: TestRootPath + "dev/.git/worktrees/folder_worktree", ExpectedRealFolder: TestRootPath + "dev/worktree", ExpectedRootFolder: TestRootPath + dotGit, }, { Case: "submodule", ExpectedEnabled: true, WorkingFolder: "./.git/modules/submodule", ExpectedWorkingFolder: TestRootPath + dotGitSubmodule, ExpectedRealFolder: TestRootPath + dotGitSubmodule, ExpectedRootFolder: TestRootPath + dotGitSubmodule, }, { Case: "submodule with root working folder", ExpectedEnabled: true, WorkingFolder: TestRootPath + dotGitSubmodule, ExpectedWorkingFolder: TestRootPath + dotGitSubmodule, ExpectedRealFolder: TestRootPath + dotGitSubmodule, ExpectedRootFolder: TestRootPath + dotGitSubmodule, }, { Case: "submodule with worktrees", ExpectedEnabled: true, WorkingFolder: TestRootPath + "dev/.git/modules/module/path/worktrees/location", WorkingFolderAddon: "gitdir", WorkingFolderContent: TestRootPath + "dev/worktree.git\n", ExpectedWorkingFolder: TestRootPath + "dev/.git/modules/module/path", ExpectedRealFolder: TestRootPath + "dev/worktree", ExpectedRootFolder: TestRootPath + "dev/.git/modules/module/path", }, { Case: "separate git dir", ExpectedEnabled: true, WorkingFolder: TestRootPath + "dev/separate/.git/posh", ExpectedWorkingFolder: TestRootPath + "dev/", ExpectedRealFolder: TestRootPath + "dev/", ExpectedRootFolder: TestRootPath + "dev/separate/.git/posh", }, { Case: "worktree with relative gitdir path", ExpectedEnabled: true, WorkingFolder: TestRootPath + "dev/.git/worktrees/folder_worktree", WorkingFolderAddon: "gitdir", WorkingFolderContent: "../../../worktree/.git\n", ExpectedWorkingFolder: TestRootPath + "dev/.git/worktrees/folder_worktree", ExpectedRealFolder: TestRootPath + "dev/worktree", ExpectedRootFolder: TestRootPath + dotGit, }, } fileInfo := &runtime.FileInfo{ Path: TestRootPath + dotGit, ParentFolder: TestRootPath + "dev", } for _, tc := range cases { env := new(mock.Environment) env.On("FileContent", TestRootPath+dotGit).Return(fmt.Sprintf("gitdir: %s", tc.WorkingFolder)) env.On("FileContent", filepath.Join(tc.WorkingFolder, tc.WorkingFolderAddon)).Return(tc.WorkingFolderContent) env.On("HasFilesInDir", tc.WorkingFolder, tc.WorkingFolderAddon).Return(true) env.On("HasFilesInDir", tc.WorkingFolder, "HEAD").Return(true) env.On("PathSeparator").Return(string(os.PathSeparator)) g := &Git{} g.Init(options.Map{}, env) assert.Equal(t, tc.ExpectedEnabled, g.hasWorktree(fileInfo), tc.Case) assert.Equal(t, tc.ExpectedWorkingFolder, g.mainSCMDir, tc.Case) assert.Equal(t, tc.ExpectedRealFolder, g.repoRootDir, tc.Case) assert.Equal(t, tc.ExpectedRootFolder, g.scmDir, tc.Case) } } func TestEnabledInBareRepo(t *testing.T) { cases := []struct { Case string HEAD string IsBare bool }{ { Case: "Bare repo on main", IsBare: true, HEAD: "ref: refs/heads/main", }, { Case: "Not a bare repo", HEAD: "ref: refs/heads/main", IsBare: false, }, } for _, tc := range cases { path := "git" env := new(mock.Environment) env.On("InWSLSharedDrive").Return(false) env.On("GOOS").Return("") env.On("HasCommand", "git").Return(true) configData := fmt.Sprintf(`[core] bare = %s`, strconv.FormatBool(tc.IsBare)) env.On("HasParentFilePath", ".git", true).Return(&runtime.FileInfo{IsDir: true, Path: path}, nil) env.On("FileContent", "git/HEAD").Return(tc.HEAD) props := options.Map{ FetchBareInfo: true, } g := &Git{} g.Init(props, env) g.configOnce = sync.Once{} g.configOnce.Do(func() { g.config, g.configErr = ini.Load([]byte(configData)) }) _ = g.Enabled() assert.Equal(t, tc.IsBare, g.IsBare, tc.Case) } } func TestGetGitOutputForCommand(t *testing.T) { args := []string{"-C", "", "--no-optional-locks", "-c", "core.quotepath=false", "-c", "color.status=false"} commandArgs := []string{"symbolic-ref", "--short", "HEAD"} want := "je suis le output" env := new(mock.Environment) env.On("IsWsl").Return(false) env.On("RunCommand", "git", append(args, commandArgs...)).Return(want, nil) env.On("GOOS").Return("unix") g := &Git{ Scm: Scm{ command: GITCOMMAND, }, } g.Init(options.Map{}, env) got := g.getGitCommandOutput(commandArgs...) assert.Equal(t, want, got) } func TestSetGitHEADContextClean(t *testing.T) { cases := []struct { Ours string Expected string Ref string Case string Total string Step string Theirs string RebaseMerge bool Sequencer bool Revert bool CherryPick bool Merge bool RebaseApply bool }{ {Case: "detached on commit", Ref: DETACHED, Expected: "branch detached at commit 1234567"}, {Case: "not detached, clean", Ref: "main", Expected: "branch main"}, { Case: "rebase merge", Ref: DETACHED, Expected: "rebase branch origin/main onto branch main (1/2) at commit 1234567", RebaseMerge: true, Ours: "refs/heads/origin/main", Theirs: "main", Step: "1", Total: "2", }, { Case: "rebase apply", Ref: DETACHED, Expected: "rebase branch origin/main (1/2) at commit 1234567", RebaseApply: true, Ours: "refs/heads/origin/main", Step: "1", Total: "2", }, { Case: "merge branch", Ref: "main", Expected: "merge branch feat-1 into branch main", Merge: true, Theirs: "branch 'feat-1'", Ours: "main", }, { Case: "merge commit", Ref: "main", Expected: "merge commit 1234567 into branch main", Merge: true, Theirs: "commit '123456789101112'", Ours: "main", }, { Case: "merge tag", Ref: "main", Expected: "merge tag 1.2.4 into branch main", Merge: true, Theirs: "tag '1.2.4'", Ours: "main", }, { Case: "cherry pick", Ref: "main", Expected: "pick commit 1234567 onto branch main", CherryPick: true, Theirs: "123456789101012", Ours: "main", }, { Case: "revert", Ref: "main", Expected: "revert commit 1234567 onto branch main", Revert: true, Theirs: "123456789101012", Ours: "main", }, { Case: "sequencer cherry", Ref: "main", Expected: "pick commit 1234567 onto branch main", Sequencer: true, Theirs: "pick 123456789101012", Ours: "main", }, { Case: "sequencer cherry p", Ref: "main", Expected: "pick commit 1234567 onto branch main", Sequencer: true, Theirs: "p 123456789101012", Ours: "main", }, { Case: "sequencer revert", Ref: "main", Expected: "revert commit 1234567 onto branch main", Sequencer: true, Theirs: "revert 123456789101012", Ours: "main", }, } for _, tc := range cases { env := new(mock.Environment) env.On("InWSLSharedDrive").Return(false) env.On("GOOS").Return("unix") env.On("IsWsl").Return(false) env.MockGitCommand("", "1234567890abcdef1234567890abcdef12345678", "rev-parse", "HEAD") env.MockGitCommand("", "", "describe", "--tags", "--exact-match") env.MockGitCommand("", tc.Theirs, "name-rev", "--name-only", "--exclude=tags/*", tc.Theirs) env.MockGitCommand("", tc.Ours, "name-rev", "--name-only", "--exclude=tags/*", tc.Ours) // rebase merge env.On("HasFolder", "/rebase-merge").Return(tc.RebaseMerge) env.On("FileContent", "/rebase-merge/head-name").Return(tc.Ours) env.On("FileContent", "/rebase-merge/onto").Return(tc.Theirs) env.On("FileContent", "/rebase-merge/msgnum").Return(tc.Step) env.On("FileContent", "/rebase-merge/end").Return(tc.Total) // rebase apply env.On("HasFolder", "/rebase-apply").Return(tc.RebaseApply) env.On("FileContent", "/rebase-apply/head-name").Return(tc.Ours) env.On("FileContent", "/rebase-apply/next").Return(tc.Step) env.On("FileContent", "/rebase-apply/last").Return(tc.Total) // merge env.On("HasFilesInDir", "", "MERGE_MSG").Return(tc.Merge) env.On("FileContent", "/MERGE_MSG").Return(fmt.Sprintf("Merge %s into %s", tc.Theirs, tc.Ours)) // cherry pick env.On("HasFilesInDir", "", "CHERRY_PICK_HEAD").Return(tc.CherryPick) env.On("FileContent", "/CHERRY_PICK_HEAD").Return(tc.Theirs) // revert env.On("HasFilesInDir", "", "REVERT_HEAD").Return(tc.Revert) env.On("FileContent", "/REVERT_HEAD").Return(tc.Theirs) // sequencer env.On("HasFilesInDir", "", "sequencer/todo").Return(tc.Sequencer) env.On("FileContent", "/sequencer/todo").Return(tc.Theirs) props := options.Map{ BranchIcon: "branch ", CommitIcon: "commit ", RebaseIcon: "rebase ", MergeIcon: "merge ", CherryPickIcon: "pick ", TagIcon: "tag ", RevertIcon: "revert ", } g := &Git{ Scm: Scm{ command: GITCOMMAND, }, ShortHash: "1234567", Ref: tc.Ref, } g.Init(props, env) g.mainSCMDir = "" g.setHEADStatus() assert.Equal(t, tc.Expected, g.HEAD, tc.Case) } } func TestSetPrettyHEADName(t *testing.T) { cases := []struct { Case string Expected string ShortHash string Tag string HEAD string SymbolicName string }{ {Case: "main", Expected: "branch main", HEAD: BRANCHPREFIX + "main"}, {Case: "no hash", Expected: "commit 1234567", HEAD: "12345678910"}, {Case: "hash on tag", ShortHash: "132312322321", Expected: "tag tag-1", HEAD: "12345678910", Tag: "tag-1"}, {Case: "no hash on tag", Expected: "tag tag-1", Tag: "tag-1"}, {Case: "hash on commit", ShortHash: "1234567", Expected: "commit 1234567"}, {Case: "no hash on commit", Expected: "commit 1234567", HEAD: "12345678910"}, {Case: "reftable main branch", Expected: "branch main", HEAD: "ref: refs/heads/.invalid", SymbolicName: "refs/heads/main"}, {Case: "reftable detached head", Expected: "commit 1234567", HEAD: "ref: refs/heads/.invalid", SymbolicName: "fatal: ref HEAD is not a symbolic ref"}, } for _, tc := range cases { env := new(mock.Environment) env.On("FileContent", "/HEAD").Return(tc.HEAD) env.On("GOOS").Return("unix") env.On("IsWsl").Return(false) // Mock rev-parse HEAD for detached HEAD cases headValue := tc.HEAD if headValue == "" || strings.HasSuffix(tc.HEAD, ".invalid") { headValue = "12345678910" } env.MockGitCommand("", headValue, "rev-parse", "HEAD") env.MockGitCommand("", tc.Tag, "describe", "--tags", "--exact-match") env.MockGitCommand("", tc.SymbolicName, "rev-parse", "--symbolic-full-name", "HEAD") props := options.Map{ BranchIcon: "branch ", CommitIcon: "commit ", TagIcon: "tag ", } g := &Git{ Scm: Scm{ command: GITCOMMAND, }, ShortHash: tc.ShortHash, } g.Init(props, env) g.mainSCMDir = "" g.updateHEADReference() assert.Equal(t, tc.Expected, g.HEAD, tc.Case) } } func TestSetGitStatus(t *testing.T) { cases := []struct { ExpectedWorking *GitStatus ExpectedStaging *GitStatus Case string Output string ExpectedHash string ExpectedRef string ExpectedUpstream string ExpectedAhead int ExpectedBehind int ExpectedUpstreamGone bool Rebase bool Merge bool }{ { Case: "all different options on working and staging, no remote", Output: ` # branch.oid 1234567891011121314 # branch.head rework-git-status 1 .R N... 1 .C N... 1 .M N... 1 .m N... 1 .A N... 1 .D N... 1 .A N... 1 .U N... 1 A. N... `, ExpectedWorking: &GitStatus{ScmStatus: ScmStatus{Modified: 4, Added: 3, Deleted: 1, Unmerged: 1}}, ExpectedStaging: &GitStatus{ScmStatus: ScmStatus{Added: 1}}, ExpectedHash: "1234567", ExpectedRef: "rework-git-status", ExpectedUpstreamGone: true, }, { Case: "all different options on working and staging, with remote", Output: ` # branch.oid 1234567891011121314 # branch.head rework-git-status # branch.upstream origin/rework-git-status # branch.ab +0 -0 1 .R N... 1 .C N... 1 .M N... 1 .m N... 1 .A N... 1 .D N... 1 .A N... 1 .U N... 1 A. N... `, ExpectedWorking: &GitStatus{ScmStatus: ScmStatus{Modified: 4, Added: 3, Deleted: 1, Unmerged: 1}}, ExpectedStaging: &GitStatus{ScmStatus: ScmStatus{Added: 1}}, ExpectedUpstream: "origin/rework-git-status", ExpectedHash: "1234567", ExpectedRef: "rework-git-status", }, { Case: "remote with equal branch", Output: ` # branch.oid 1234567891011121314 # branch.head rework-git-status # branch.upstream origin/rework-git-status # branch.ab +0 -0 `, ExpectedUpstream: "origin/rework-git-status", ExpectedHash: "1234567", ExpectedRef: "rework-git-status", }, { Case: "remote with branch status", Output: ` # branch.oid 1234567891011121314 # branch.head rework-git-status # branch.upstream origin/rework-git-status # branch.ab +2 -1 `, ExpectedUpstream: "origin/rework-git-status", ExpectedHash: "1234567", ExpectedRef: "rework-git-status", ExpectedAhead: 2, ExpectedBehind: 1, }, { Case: "untracked files", Output: ` # branch.oid 1234567891011121314 # branch.head main # branch.upstream origin/main # branch.ab +0 -0 ? q ? qq ? qqq `, ExpectedUpstream: "origin/main", ExpectedHash: "1234567", ExpectedRef: "main", ExpectedWorking: &GitStatus{ScmStatus: ScmStatus{Untracked: 3}}, }, { Case: "remote branch was deleted", Output: ` # branch.oid 1234567891011121314 # branch.head branch-is-gone # branch.upstream origin/branch-is-gone `, ExpectedUpstream: "origin/branch-is-gone", ExpectedHash: "1234567", ExpectedRef: "branch-is-gone", ExpectedUpstreamGone: true, }, { Case: "rebase with 2 merge conflicts", Output: ` # branch.oid 1234567891011121314 # branch.head rework-git-status # branch.upstream origin/rework-git-status # branch.ab +0 -0 1 AA N... 1 AA N... `, ExpectedUpstream: "origin/rework-git-status", ExpectedHash: "1234567", ExpectedRef: "rework-git-status", Rebase: true, ExpectedStaging: &GitStatus{ScmStatus: ScmStatus{Unmerged: 2}}, }, { Case: "merge with 4 merge conflicts", Output: ` # branch.oid 1234567891011121314 # branch.head rework-git-status # branch.upstream origin/rework-git-status # branch.ab +0 -0 1 AA N... 1 AA N... 1 AA N... 1 AA N... `, ExpectedUpstream: "origin/rework-git-status", ExpectedHash: "1234567", ExpectedRef: "rework-git-status", Merge: true, ExpectedStaging: &GitStatus{ScmStatus: ScmStatus{Unmerged: 4}}, }, { Case: "staged new file also appears in working", Output: ` # branch.oid 1234567891011121314 # branch.head main # branch.upstream origin/main # branch.ab +0 -0 1 .M N... 1 A. N... ? untracked1 ? untracked2 ? untracked3 ? untracked4 `, ExpectedUpstream: "origin/main", ExpectedHash: "1234567", ExpectedRef: "main", ExpectedWorking: &GitStatus{ScmStatus: ScmStatus{Modified: 1, Added: 1, Untracked: 4}}, ExpectedStaging: &GitStatus{ScmStatus: ScmStatus{Added: 1}}, }, } for _, tc := range cases { env := new(mock.Environment) env.On("GOOS").Return("unix") env.On("IsWsl").Return(false) env.MockGitCommand("", strings.ReplaceAll(tc.Output, "\t", ""), "status", "-unormal", "--branch", "--porcelain=2") g := &Git{ Scm: Scm{ command: GITCOMMAND, }, } g.Init(options.Map{}, env) if tc.ExpectedWorking == nil { tc.ExpectedWorking = &GitStatus{} } if tc.ExpectedStaging == nil { tc.ExpectedStaging = &GitStatus{} } if tc.Rebase { g.Rebase = &Rebase{} } g.Merge = tc.Merge tc.ExpectedStaging.Formats = map[string]string{} tc.ExpectedWorking.Formats = map[string]string{} g.setStatus() assert.Equal(t, tc.ExpectedStaging, g.Staging, tc.Case) assert.Equal(t, tc.ExpectedWorking, g.Working, tc.Case) assert.Equal(t, tc.ExpectedHash, g.ShortHash, tc.Case) assert.Equal(t, tc.ExpectedRef, g.Ref, tc.Case) assert.Equal(t, tc.ExpectedUpstream, g.Upstream, tc.Case) assert.Equal(t, tc.ExpectedUpstreamGone, g.UpstreamGone, tc.Case) assert.Equal(t, tc.ExpectedAhead, g.Ahead, tc.Case) assert.Equal(t, tc.ExpectedBehind, g.Behind, tc.Case) } } func TestGetStashContextZeroEntries(t *testing.T) { cases := []struct { StashContent string Expected int }{ {Expected: 0, StashContent: ""}, {Expected: 2, StashContent: "1\n2\n"}, {Expected: 4, StashContent: "1\n2\n3\n4\n\n"}, } for _, tc := range cases { env := new(mock.Environment) env.On("FileContent", "/logs/refs/stash").Return(tc.StashContent) g := &Git{ Scm: Scm{ mainSCMDir: "", }, } g.Init(options.Map{}, env) got := g.StashCount() assert.Equal(t, tc.Expected, got) } } func TestGitCleanSSHURL(t *testing.T) { cases := []struct { Case string Expected string Upstream string }{ {Case: "regular URL", Expected: "https://src.example.com/user/repo", Upstream: "/src.example.com/user/repo.git"}, {Case: "domain:path", Expected: "https://host.xz/path/to/repo", Upstream: "host.xz:/path/to/repo.git/"}, {Case: "ssh with port", Expected: "https://host.xz/path/to/repo", Upstream: "ssh://user@host.xz:1234/path/to/repo.git"}, {Case: "ssh with 3-digit port", Expected: "https://host.xz/path/to/repo", Upstream: "ssh://user@host.xz:234/path/to/repo.git"}, {Case: "ssh with port, trailing slash", Expected: "https://host.xz/path/to/repo", Upstream: "ssh://user@host.xz:1234/path/to/repo.git/"}, {Case: "ssh without port", Expected: "https://host.xz/path/to/repo", Upstream: "ssh://user@host.xz/path/to/repo.git/"}, {Case: "ssh port, no user", Expected: "https://host.xz/path/to/repo", Upstream: "ssh://host.xz:1234/path/to/repo.git"}, {Case: "ssh no port, no user", Expected: "https://host.xz/path/to/repo", Upstream: "ssh://host.xz/path/to/repo.git"}, {Case: "rsync no port, no user", Expected: "https://host.xz/path/to/repo", Upstream: "rsync://host.xz/path/to/repo.git/"}, {Case: "git no port, no user", Expected: "https://host.xz/path/to/repo", Upstream: "git://host.xz/path/to/repo.git"}, {Case: "gitea no port, no user", Expected: "https://src.example.com/user/repo", Upstream: "_gitea@src.example.com:user/repo.git"}, {Case: "git@ with user", Expected: "https://github.com/JanDeDobbeleer/oh-my-posh", Upstream: "git@github.com:JanDeDobbeleer/oh-my-posh"}, {Case: "unsupported", Upstream: "\\test\\repo.git"}, {Case: "Azure DevOps, https", Expected: "https://dev.azure.com/posh/oh-my-posh/_git/website", Upstream: "https://posh@dev.azure.com/posh/oh-my-posh/_git/website"}, {Case: "Azure DevOps, ssh", Expected: "https://dev.azure.com/posh/oh-my-posh/_git/website", Upstream: "git@ssh.dev.azure.com:v3/posh/oh-my-posh/website"}, } for _, tc := range cases { g := &Git{} upstreamURL := g.cleanUpstreamURL(tc.Upstream) assert.Equal(t, tc.Expected, upstreamURL, tc.Case) } } func TestGitUpstream(t *testing.T) { cases := []struct { Case string Expected string Upstream string }{ {Case: "No upstream", Expected: "G", Upstream: ""}, {Case: "SSH url", Expected: "G", Upstream: "ssh://git@git.my.domain:3001/ADIX7/dotconfig.git"}, {Case: "Gitea", Expected: "EX", Upstream: "_gitea@src.example.com:user/repo.git"}, {Case: "GitHub", Expected: "GH", Upstream: "github.com/test"}, {Case: "GitLab", Expected: "GL", Upstream: "gitlab.com/test"}, {Case: "Bitbucket", Expected: "BB", Upstream: "bitbucket.org/test"}, {Case: "Azure DevOps", Expected: "AD", Upstream: "dev.azure.com/test"}, {Case: "Azure DevOps Dos", Expected: "AD", Upstream: "test.visualstudio.com"}, {Case: "CodeCommit", Expected: "AC", Upstream: "codecommit::eu-west-1://test-repository"}, {Case: "Codeberg", Expected: "CB", Upstream: "codeberg.org:user/repo.git"}, {Case: "Gitstash", Expected: "G", Upstream: "gitstash.com/test"}, {Case: "My custom server", Expected: "CU", Upstream: "mycustom.server/test"}, {Case: "GitHub with dash", Expected: "GH", Upstream: "github.com:pixel48/custom-reg"}, } for _, tc := range cases { env := &mock.Environment{} env.On("IsWsl").Return(false) env.On("RunCommand", "git", []string{"-C", "", "--no-optional-locks", "-c", "core.quotepath=false", "-c", "color.status=false", "remote", "get-url", origin}).Return(tc.Upstream, nil) env.On("GOOS").Return("unix") props := options.Map{ GithubIcon: "GH", GitlabIcon: "GL", BitbucketIcon: "BB", AzureDevOpsIcon: "AD", CodeCommit: "AC", CodebergIcon: "CB", GitIcon: "G", UpstreamIcons: map[string]string{ "mycustom.server": "CU", "src.example.com": "EX", }, } g := &Git{ Scm: Scm{ command: GITCOMMAND, Upstream: "origin/main", }, } g.Init(props, env) g.configOnce = sync.Once{} g.configOnce.Do(func() { g.configErr = errors.New("no config") }) upstreamIcon := g.getUpstreamIcon() assert.Equal(t, tc.Expected, upstreamIcon, tc.Case) } } func TestGetBranchStatus(t *testing.T) { cases := []struct { Case string Expected string Upstream string Ahead int Behind int UpstreamGone bool }{ {Case: "Equal with remote", Expected: "equal", Upstream: branchName}, {Case: "Ahead", Expected: "up2", Ahead: 2}, {Case: "Behind", Expected: "down8", Behind: 8}, {Case: "Behind and ahead", Expected: "up7 down8", Behind: 8, Ahead: 7}, {Case: "Gone", Expected: "gone", Upstream: branchName, UpstreamGone: true}, {Case: "No remote", Expected: "", Upstream: ""}, {Case: "Default (bug)", Expected: "", Behind: -8, Upstream: "wonky"}, } for _, tc := range cases { props := options.Map{ BranchAheadIcon: "up", BranchBehindIcon: "down", BranchIdenticalIcon: "equal", BranchGoneIcon: "gone", } g := &Git{ Scm: Scm{ Upstream: tc.Upstream, }, Ahead: tc.Ahead, Behind: tc.Behind, UpstreamGone: tc.UpstreamGone, } g.Init(props, new(mock.Environment)) g.setBranchStatus() assert.Equal(t, tc.Expected, g.BranchStatus, tc.Case) } } func TestGitTemplateString(t *testing.T) { cases := []struct { Git *Git Case string Expected string Template string }{ { Case: "Only HEAD name", Expected: branchName, Template: "{{ .HEAD }}", Git: &Git{ HEAD: branchName, Behind: 2, }, }, { Case: "Working area changes", Expected: "main \uF044 +2 ~3", Template: "{{ .HEAD }}{{ if .Working.Changed }} \uF044 {{ .Working.String }}{{ end }}", Git: &Git{ HEAD: branchName, Working: &GitStatus{ ScmStatus: ScmStatus{ Added: 2, Modified: 3, }, }, }, }, { Case: "No working area changes", Expected: branchName, Template: "{{ .HEAD }}{{ if .Working.Changed }} \uF044 {{ .Working.String }}{{ end }}", Git: &Git{ HEAD: branchName, Working: &GitStatus{}, }, }, { Case: "Working and staging area changes", Expected: "main \uF046 +5 ~1 \uF044 +2 ~3", Template: "{{ .HEAD }}{{ if .Staging.Changed }} \uF046 {{ .Staging.String }}{{ end }}{{ if .Working.Changed }} \uF044 {{ .Working.String }}{{ end }}", Git: &Git{ HEAD: branchName, Working: &GitStatus{ ScmStatus: ScmStatus{ Added: 2, Modified: 3, }, }, Staging: &GitStatus{ ScmStatus: ScmStatus{ Added: 5, Modified: 1, }, }, }, }, { Case: "Working and staging area changes with separator", Expected: "main \uF046 +5 ~1 | \uF044 +2 ~3", Template: "{{ .HEAD }}{{ if .Staging.Changed }} \uF046 {{ .Staging.String }}{{ end }}{{ if and (.Working.Changed) (.Staging.Changed) }} |{{ end }}{{ if .Working.Changed }} \uF044 {{ .Working.String }}{{ end }}", //nolint:lll Git: &Git{ HEAD: branchName, Working: &GitStatus{ ScmStatus: ScmStatus{ Added: 2, Modified: 3, }, }, Staging: &GitStatus{ ScmStatus: ScmStatus{ Added: 5, Modified: 1, }, }, }, }, { Case: "Working and staging area changes with separator and stash count", Expected: "main \uF046 +5 ~1 | \uF044 +2 ~3 \ueb4b 3", Template: "{{ .HEAD }}{{ if .Staging.Changed }} \uF046 {{ .Staging.String }}{{ end }}{{ if and (.Working.Changed) (.Staging.Changed) }} |{{ end }}{{ if .Working.Changed }} \uF044 {{ .Working.String }}{{ end }}{{ if gt .StashCount 0 }} \ueb4b {{ .StashCount }}{{ end }}", //nolint:lll Git: &Git{ HEAD: branchName, Working: &GitStatus{ ScmStatus: ScmStatus{ Added: 2, Modified: 3, }, }, Staging: &GitStatus{ ScmStatus: ScmStatus{ Added: 5, Modified: 1, }, }, stashCount: 3, poshgit: true, }, }, { Case: "No local changes", Expected: branchName, Template: "{{ .HEAD }}{{ if .Staging.Changed }} \uF046{{ .Staging.String }}{{ end }}{{ if .Working.Changed }} \uF044{{ .Working.String }}{{ end }}", Git: &Git{ HEAD: branchName, Staging: &GitStatus{}, Working: &GitStatus{}, }, }, { Case: "Upstream Icon", Expected: "from GitHub on main", Template: "from {{ .UpstreamIcon }} on {{ .HEAD }}", Git: &Git{ HEAD: branchName, Staging: &GitStatus{}, Working: &GitStatus{}, UpstreamIcon: "GitHub", }, }, } for _, tc := range cases { props := options.Map{ FetchStatus: true, } env := new(mock.Environment) tc.Git.env = env tc.Git.options = props assert.Equal(t, tc.Expected, renderTemplate(env, tc.Template, tc.Git), tc.Case) } } func TestGitUntrackedMode(t *testing.T) { cases := []struct { UntrackedModes map[string]string Case string Expected string }{ { Case: "Default mode - no map", Expected: "-unormal", }, { Case: "Default mode - no match", Expected: "-unormal", UntrackedModes: map[string]string{ "bar": "no", }, }, { Case: "No mode - match", Expected: "-uno", UntrackedModes: map[string]string{ "foo": "no", "bar": "normal", }, }, { Case: "Global mode", Expected: "-uno", UntrackedModes: map[string]string{ "*": "no", }, }, } for _, tc := range cases { props := options.Map{ UntrackedModes: tc.UntrackedModes, } g := &Git{ Scm: Scm{ repoRootDir: "foo", }, } g.Init(props, new(mock.Environment)) got := g.getUntrackedFilesMode() assert.Equal(t, tc.Expected, got, tc.Case) } } func TestGitIgnoreSubmodules(t *testing.T) { cases := []struct { IgnoreSubmodules map[string]string Case string Expected string }{ { Case: "Override", Expected: "--ignore-submodules=all", IgnoreSubmodules: map[string]string{ "foo": "all", }, }, { Case: "Default mode - empty", IgnoreSubmodules: map[string]string{ "bar": "no", }, }, { Case: "Global mode", Expected: "--ignore-submodules=dirty", IgnoreSubmodules: map[string]string{ "*": "dirty", }, }, } for _, tc := range cases { props := options.Map{ IgnoreSubmodules: tc.IgnoreSubmodules, } g := &Git{ Scm: Scm{ repoRootDir: "foo", }, } g.Init(props, new(mock.Environment)) got := g.getIgnoreSubmodulesMode() assert.Equal(t, tc.Expected, got, tc.Case) } } func TestGitCommit(t *testing.T) { cases := []struct { Case string Expected *Commit Output string }{ { Case: "Clean commit", Output: ` an:Jan De Dobbeleer ae:jan@ohmyposh.dev cn:Jan De Dobbeleer ce:jan@ohmyposh.dev at:1673176335 su:docs(error): you can't use cross segment properties ha:1234567891011121314 rf:HEAD -> refs/heads/main, tag: refs/tags/tag-1, tag: refs/tags/0.3.4, refs/remotes/origin/main, refs/remotes/origin/dev, refs/heads/dev, refs/remotes/origin/HEAD `, Expected: &Commit{ Author: &User{ Name: "Jan De Dobbeleer", Email: "jan@ohmyposh.dev", }, Committer: &User{ Name: "Jan De Dobbeleer", Email: "jan@ohmyposh.dev", }, Subject: "docs(error): you can't use cross segment properties", Timestamp: time.Unix(1673176335, 0), Refs: &Refs{ Tags: []string{"tag-1", "0.3.4"}, Heads: []string{"main", "dev"}, Remotes: []string{"origin/main", "origin/dev"}, }, Sha: "1234567891011121314", }, }, { Case: "No commit output", Expected: &Commit{ Author: &User{}, Committer: &User{}, Refs: &Refs{}, }, }, { Case: "No author", Output: ` an: ae: cn:Jan De Dobbeleer ce:jan@ohmyposh.dev at:1673176335 su:docs(error): you can't use cross segment properties `, Expected: &Commit{ Author: &User{}, Committer: &User{ Name: "Jan De Dobbeleer", Email: "jan@ohmyposh.dev", }, Subject: "docs(error): you can't use cross segment properties", Timestamp: time.Unix(1673176335, 0), Refs: &Refs{}, }, }, { Case: "No refs", Output: ` rf:HEAD `, Expected: &Commit{ Author: &User{}, Committer: &User{}, Refs: &Refs{}, }, }, { Case: "Just tag ref", Output: ` rf:HEAD, tag: refs/tags/tag-1 `, Expected: &Commit{ Author: &User{}, Committer: &User{}, Refs: &Refs{ Tags: []string{"tag-1"}, }, }, }, { Case: "Feature branch including slash", Output: ` rf:HEAD, tag: refs/tags/feat/feat-1 `, Expected: &Commit{ Author: &User{}, Committer: &User{}, Refs: &Refs{ Tags: []string{"feat/feat-1"}, }, }, }, { Case: "Bad timestamp", Output: ` at:err `, Expected: &Commit{ Author: &User{}, Committer: &User{}, Refs: &Refs{}, }, }, } for _, tc := range cases { env := new(mock.Environment) env.MockGitCommand("", tc.Output, "log", "-1", "--pretty=format:an:%an%nae:%ae%ncn:%cn%nce:%ce%nat:%at%nsu:%s%nha:%H%nrf:%D", "--decorate=full") g := &Git{ Scm: Scm{ command: GITCOMMAND, }, } g.Init(options.Map{}, env) got := g.Commit() assert.Equal(t, tc.Expected, got, tc.Case) } } func TestGitRemotes(t *testing.T) { cases := []struct { ExpectedRemotes map[string]string Case string Config string Expected int }{ { Case: "Empty config file", Expected: 0, ExpectedRemotes: map[string]string{}, }, { Case: "Two remotes", Expected: 2, Config: ` [remote "origin"] url = git@github.com:JanDeDobbeleer/test.git fetch = +refs/heads/*:refs/remotes/origin/* [remote "upstream"] url = git@github.com:microsoft/test.git fetch = +refs/heads/*:refs/remotes/upstream/* `, ExpectedRemotes: map[string]string{ "origin": "https://github.com/JanDeDobbeleer/test", "upstream": "https://github.com/microsoft/test", }, }, { Case: "One remote", Expected: 1, Config: ` [remote "origin"] url = git@github.com:JanDeDobbeleer/test.git fetch = +refs/heads/*:refs/remotes/origin/* `, ExpectedRemotes: map[string]string{ "origin": "https://github.com/JanDeDobbeleer/test", }, }, { Case: "Broken config", Expected: 0, Config: "{{}}", ExpectedRemotes: map[string]string{}, }, { Case: "Three remotes with different URL formats", Expected: 3, Config: ` [remote "origin"] url = git@github.com:JanDeDobbeleer/test.git fetch = +refs/heads/*:refs/remotes/origin/* [remote "upstream"] url = https://github.com/microsoft/test.git fetch = +refs/heads/*:refs/remotes/upstream/* [remote "fork"] url = git@gitlab.com:user/test.git fetch = +refs/heads/*:refs/remotes/fork/* `, ExpectedRemotes: map[string]string{ "origin": "https://github.com/JanDeDobbeleer/test", "upstream": "https://github.com/microsoft/test.git", "fork": "https://gitlab.com/user/test", }, }, } for _, tc := range cases { env := new(mock.Environment) g := &Git{ Scm: Scm{ repoRootDir: "foo", }, } g.Init(options.Map{}, env) g.configOnce = sync.Once{} g.configOnce.Do(func() { g.config, g.configErr = ini.Load([]byte(tc.Config)) }) got := g.Remotes() assert.Equal(t, tc.Expected, len(got), tc.Case) // Verify the actual remote names and URLs for name, expectedURL := range tc.ExpectedRemotes { actualURL, exists := got[name] assert.True(t, exists, "%s: expected remote '%s' to exist", tc.Case, name) assert.Equal(t, expectedURL, actualURL, "%s: remote '%s' URL mismatch", tc.Case, name) } } } func TestGitRepoName(t *testing.T) { cases := []struct { Case string Expected string WorkingDir string RealDir string IsWorkTree bool }{ { Case: "In worktree", Expected: "oh-my-posh", IsWorkTree: true, WorkingDir: "/Users/jan/Code/oh-my-posh/.git/worktrees/oh-my-posh2", }, { Case: "Not in worktree", Expected: "oh-my-posh", IsWorkTree: false, RealDir: "/Users/jan/Code/oh-my-posh", }, { Case: "In worktree, unexpected dir", Expected: "", IsWorkTree: true, WorkingDir: "/Users/jan/Code/oh-my-posh2", }, } for _, tc := range cases { env := new(mock.Environment) env.On("PathSeparator").Return("/") env.On("GOOS").Return(runtime.LINUX) g := &Git{ Scm: Scm{ repoRootDir: tc.RealDir, mainSCMDir: tc.WorkingDir, }, IsWorkTree: tc.IsWorkTree, } g.Init(options.Map{}, env) got := g.repoName() assert.Equal(t, tc.Expected, got, tc.Case) } } func TestDisableWithJJEnabled(t *testing.T) { env := new(mock.Environment) env.On("InWSLSharedDrive").Return(false) env.On("GOOS").Return("") env.On("IsWsl").Return(false) // Mock .jj directory exists env.On("HasParentFilePath", ".jj", false).Return(&runtime.FileInfo{Path: "/dir/.jj", IsDir: true}, nil) g := &Git{} props := options.Map{ DisableWithJJ: true, } g.Init(props, env) assert.False(t, g.Enabled()) } func TestDisableWithJJDisabled(t *testing.T) { fileInfo := &runtime.FileInfo{ Path: "/dir/.git", ParentFolder: "/dir", IsDir: true, } env := new(mock.Environment) env.On("InWSLSharedDrive").Return(false) env.On("HasCommand", "git").Return(true) env.On("GOOS").Return("") env.On("FileContent", "/dir/.git/HEAD").Return("") env.MockGitCommand("/dir", "1234567890abcdef1234567890abcdef12345678", "rev-parse", "HEAD") env.MockGitCommand("/dir", "", "describe", "--tags", "--exact-match") // Use repo root, not .git dir env.On("IsWsl").Return(false) // Mock .jj directory exists env.On("HasParentFilePath", ".jj", false).Return(&runtime.FileInfo{Path: "/dir/.jj", IsDir: true}, nil) env.On("HasParentFilePath", ".git", true).Return(fileInfo, nil) env.On("PathSeparator").Return("/") env.On("Home").Return(poshHome) env.On("Getenv", poshGitEnv).Return("") env.On("DirMatchesOneOf", testify_.Anything, testify_.Anything).Return(false) g := &Git{} props := options.Map{ DisableWithJJ: false, // Property is disabled } g.Init(props, env) assert.True(t, g.Enabled()) // Should still be enabled since disable_with_jj is false } func TestDisableWithJJNoJJDirectory(t *testing.T) { fileInfo := &runtime.FileInfo{ Path: "/dir/.git", ParentFolder: "/dir", IsDir: true, } env := new(mock.Environment) env.On("InWSLSharedDrive").Return(false) env.On("HasCommand", "git").Return(true) env.On("GOOS").Return("") env.On("FileContent", "/dir/.git/HEAD").Return("") env.MockGitCommand("/dir", "1234567890abcdef1234567890abcdef12345678", "rev-parse", "HEAD") env.MockGitCommand("/dir", "", "describe", "--tags", "--exact-match") // Use repo root, not .git dir env.On("IsWsl").Return(false) // Mock .jj directory does not exist env.On("HasParentFilePath", ".jj", false).Return((*runtime.FileInfo)(nil), errors.New("no .jj found")) env.On("HasParentFilePath", ".git", true).Return(fileInfo, nil) env.On("PathSeparator").Return("/") env.On("Home").Return(poshHome) env.On("Getenv", poshGitEnv).Return("") env.On("DirMatchesOneOf", testify_.Anything, testify_.Anything).Return(false) g := &Git{} props := options.Map{ DisableWithJJ: true, // Property is enabled but no .jj directory } g.Init(props, env) assert.True(t, g.Enabled()) // Should be enabled since .jj directory doesn't exist } func TestPushStatusAheadAndBehind(t *testing.T) { cases := []struct { Case string PushAheadCount string PushBehindCount string Config string ExpectedPushAhead int ExpectedPushBehind int }{ { Case: "ahead and behind", PushAheadCount: "3", PushBehindCount: "5", ExpectedPushAhead: 3, ExpectedPushBehind: 5, }, { Case: "only ahead", PushAheadCount: "2", PushBehindCount: "0", ExpectedPushAhead: 2, ExpectedPushBehind: 0, }, { Case: "only behind", PushAheadCount: "0", PushBehindCount: "7", ExpectedPushAhead: 0, ExpectedPushBehind: 7, }, { Case: "up to date", PushAheadCount: "0", PushBehindCount: "0", ExpectedPushAhead: 0, ExpectedPushBehind: 0, }, { Case: "remote from config", PushAheadCount: "2", PushBehindCount: "0", ExpectedPushAhead: 2, ExpectedPushBehind: 0, Config: ` [branch "main"] remote = origin merge = refs/heads/main `, }, } for _, tc := range cases { env := new(mock.Environment) env.On("RunCommand", "git", []string{"-C", "/dir", "--no-optional-locks", "-c", "core.quotepath=false", "-c", "color.status=false", "config", "--get", "remote.pushDefault"}).Return("", nil) env.On("RunCommand", "git", []string{"-C", "/dir", "--no-optional-locks", "-c", "core.quotepath=false", "-c", "color.status=false", "rev-list", "--count", "origin/main..HEAD"}).Return(tc.PushAheadCount, nil) env.On("RunCommand", "git", []string{"-C", "/dir", "--no-optional-locks", "-c", "core.quotepath=false", "-c", "color.status=false", "rev-list", "--count", "HEAD..origin/main"}).Return(tc.PushBehindCount, nil) env.On("FileContent", "/dir/.git/config").Return("") g := &Git{ Scm: Scm{ command: "git", repoRootDir: "/dir", scmDir: "/dir/.git", Upstream: "origin/main", }, Ref: "main", } props := options.Map{ FetchPushStatus: true, } g.Init(props, env) g.configOnce = sync.Once{} g.configOnce.Do(func() { if len(tc.Config) > 0 { g.config, g.configErr = ini.Load([]byte(tc.Config)) return } g.configErr = errors.New("no config") }) g.setPushStatus() assert.Equal(t, tc.ExpectedPushAhead, g.PushAhead, tc.Case) assert.Equal(t, tc.ExpectedPushBehind, g.PushBehind, tc.Case) } } ================================================ FILE: src/segments/git_unix.go ================================================ //go:build !windows package segments import "path/filepath" // resolveGitPath resolves path relative to base. func resolveGitPath(base, path string) string { if filepath.IsAbs(path) { return path } return filepath.Join(base, path) } ================================================ FILE: src/segments/git_unix_test.go ================================================ //go:build !windows package segments import ( "testing" "github.com/stretchr/testify/assert" ) const TestRootPath = "/" func TestResolveGitPath(t *testing.T) { cases := []struct { Case string Base string Path string Expected string }{ { Case: "relative path", Base: "dir/", Path: "sub", Expected: "dir/sub", }, { Case: "absolute path", Base: "/base", Path: "/absolute/path", Expected: "/absolute/path", }, } for _, tc := range cases { assert.Equal(t, tc.Expected, resolveGitPath(tc.Base, tc.Path), tc.Case) } } ================================================ FILE: src/segments/git_windows.go ================================================ package segments import "path/filepath" // resolveGitPath resolves path relative to base. func resolveGitPath(base, path string) string { if path == "" { return base } if filepath.IsAbs(path) { return path } // Note that git on Windows uses slashes exclusively. And it's okay // because Windows actually accepts both directory separators. More // importantly, however, parts of the git segment depend on those // slashes. if path[0] == '/' { // path is a disk-relative path. return filepath.VolumeName(base) + path } return filepath.ToSlash(filepath.Join(base, path)) } ================================================ FILE: src/segments/git_windows_test.go ================================================ //go:build windows package segments import ( "testing" "github.com/stretchr/testify/assert" ) const TestRootPath = "C:/" func TestResolveGitPath(t *testing.T) { cases := []struct { Case string Base string Path string Expected string }{ { Case: "relative path", Base: "dir\\", Path: "sub", Expected: "dir/sub", }, { Case: "absolute path", Base: "C:\\base", Path: "C:/absolute/path", Expected: "C:/absolute/path", }, { Case: "disk-relative path", Base: "C:\\base", Path: "/absolute/path", Expected: "C:/absolute/path", }, } for _, tc := range cases { assert.Equal(t, tc.Expected, resolveGitPath(tc.Base, tc.Path), tc.Case) } } ================================================ FILE: src/segments/gitversion.go ================================================ package segments import ( "encoding/json" ) type GitVersionInfo struct { NuGetVersionV2 string `json:"NuGetVersionV2"` FullSemVer string `json:"FullSemVer"` CommitDate string `json:"CommitDate"` AssemblySemVer string `json:"AssemblySemVer"` PreReleaseTagWithDash string `json:"PreReleaseTagWithDash"` PreReleaseLabel string `json:"PreReleaseLabel"` PreReleaseLabelWithDash string `json:"PreReleaseLabelWithDash"` AssemblySemFileVer string `json:"AssemblySemFileVer"` CommitsSinceVersionSourcePadded string `json:"CommitsSinceVersionSourcePadded"` VersionSourceSha string `json:"VersionSourceSha"` BuildMetaDataPadded string `json:"BuildMetaDataPadded"` FullBuildMetaData string `json:"FullBuildMetaData"` MajorMinorPatch string `json:"MajorMinorPatch"` NuGetVersion string `json:"NuGetVersion"` LegacySemVer string `json:"LegacySemVer"` LegacySemVerPadded string `json:"LegacySemVerPadded"` PreReleaseTag string `json:"PreReleaseTag"` NuGetPreReleaseTag string `json:"NuGetPreReleaseTag"` SemVer string `json:"SemVer"` InformationalVersion string `json:"InformationalVersion"` BranchName string `json:"BranchName"` EscapedBranchName string `json:"EscapedBranchName"` Sha string `json:"Sha"` ShortSha string `json:"ShortSha"` NuGetPreReleaseTagV2 string `json:"NuGetPreReleaseTagV2"` BuildMetaData int `json:"BuildMetaData"` Major int `json:"Major"` PreReleaseNumber int `json:"PreReleaseNumber"` Minor int `json:"Minor"` CommitsSinceVersionSource int `json:"CommitsSinceVersionSource"` WeightedPreReleaseNumber int `json:"WeightedPreReleaseNumber"` UncommittedChanges int `json:"UncommittedChanges"` Patch int `json:"Patch"` } type GitVersion struct { Base GitVersionInfo } func (n *GitVersion) Template() string { return " {{ .MajorMinorPatch }} " } func (n *GitVersion) Enabled() bool { gitversion := "gitversion" if !n.env.HasCommand(gitversion) { return false } response, err := n.env.RunCommand(gitversion, "-output", "json") if err != nil { return false } n.GitVersionInfo = GitVersionInfo{} err = json.Unmarshal([]byte(response), &n.GitVersionInfo) return err == nil } ================================================ FILE: src/segments/gitversion_test.go ================================================ package segments import ( "errors" "testing" "github.com/jandedobbeleer/oh-my-posh/src/runtime/mock" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" "github.com/alecthomas/assert" ) func TestGitversion(t *testing.T) { cases := []struct { CacheError error CommandError error Case string ExpectedString string Response string CacheResponse string Template string CacheTimeout int ExpectedEnabled bool HasGitversion bool }{ {Case: "GitVersion not installed"}, {Case: "GitVersion installed, no GitVersion.yml file", HasGitversion: true, Response: "Cannot find the .git directory"}, { Case: "Version", ExpectedEnabled: true, ExpectedString: "number", HasGitversion: true, Response: "{ \"FullSemVer\": \"0.1.0\", \"SemVer\": \"number\" }", Template: "{{ .SemVer }}", }, { Case: "Command Error", HasGitversion: true, CommandError: errors.New("error"), }, } for _, tc := range cases { env := new(mock.Environment) env.On("HasCommand", "gitversion").Return(tc.HasGitversion) env.On("Pwd").Return("test-dir") env.On("RunCommand", "gitversion", []string{"-output", "json"}).Return(tc.Response, tc.CommandError) gitversion := &GitVersion{} gitversion.Init(options.Map{}, env) if tc.Template == "" { tc.Template = gitversion.Template() } enabled := gitversion.Enabled() assert.Equal(t, tc.ExpectedEnabled, enabled, tc.Case) if enabled { assert.Equal(t, tc.ExpectedString, renderTemplate(env, tc.Template, gitversion), tc.Case) } } } ================================================ FILE: src/segments/golang.go ================================================ package segments import ( "github.com/jandedobbeleer/oh-my-posh/src/regex" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" "golang.org/x/mod/modfile" ) type Golang struct { Language } const ( ParseModFile options.Option = "parse_mod_file" ParseWorkFile options.Option = "parse_work_file" ) func (g *Golang) Template() string { return languageTemplate } func (g *Golang) Enabled() bool { g.extensions = []string{"*.go", "go.mod", "go.sum", "go.work", "go.work.sum"} g.tooling = map[string]*cmd{ "mod": { regex: `(?P((?P[0-9]+).(?P[0-9]+)(.(?P[0-9]+))?))`, getVersion: g.getVersion, }, "go": { executable: "go", args: []string{"version"}, regex: `(?:go(?P((?P[0-9]+).(?P[0-9]+)(.(?P[0-9]+))?)))`, }, } g.defaultTooling = []string{"mod", "go"} g.versionURLTemplate = "https://golang.org/doc/go{{ .Major }}.{{ .Minor }}" return g.Language.Enabled() } // getVersion returns the version of the Go language // It first checks if the go.mod file is present and if it is, it parses the file to get the version // If the go.mod file is not present, it checks if the go.work file is present and if it is, it parses the file to get the version // If neither file is present, it returns an empty string func (g *Golang) getVersion() (string, error) { if g.options.Bool(ParseModFile, false) { return g.parseModFile() } if g.options.Bool(ParseWorkFile, false) { return g.parseWorkFile() } return "", nil } func (g *Golang) parseModFile() (string, error) { gomod, err := g.env.HasParentFilePath("go.mod", false) if err != nil { return "", err } contents := g.env.FileContent(gomod.Path) file, err := modfile.Parse(gomod.Path, []byte(contents), nil) if err != nil { return "", err } if file.Go.Version != "" { return file.Go.Version, nil } // ignore when no version is found in go.mod file return "", nil } func (g *Golang) parseWorkFile() (string, error) { goWork, err := g.env.HasParentFilePath("go.work", false) if err != nil { return "", err } contents := g.env.FileContent(goWork.Path) version, _ := regex.FindStringMatch(`go (\d(\.\d{1,2})?(\.\d{1,2})?)`, contents, 1) if len(version) > 0 { return version, nil } // ignore when no version is found in go.work file return "", nil } ================================================ FILE: src/segments/golang_test.go ================================================ package segments import ( "errors" "fmt" "os" "testing" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/stretchr/testify/assert" ) func TestGolang(t *testing.T) { cases := []struct { Case string ExpectedString string Version string ParseModFile bool HasModFileInParentDir bool InvalidModfile bool ParseGoWorkFile bool HasGoWorkFileInParentDir bool InvalidGoWorkFile bool }{ {Case: "Go 1.15", ExpectedString: "1.15.8", Version: "go version go1.15.8 darwin/amd64"}, {Case: "Go 1.16", ExpectedString: "1.16", Version: "go version go1.16 darwin/amd64"}, {Case: "go.mod 1.26.0", ParseModFile: true, HasModFileInParentDir: true, ExpectedString: "1.26.0"}, {Case: "no go.mod file fallback", ParseModFile: true, ExpectedString: "1.16", Version: "go version go1.16 darwin/amd64"}, { Case: "invalid go.mod file fallback", ParseModFile: true, HasModFileInParentDir: true, InvalidModfile: true, ExpectedString: "1.16", Version: "go version go1.16 darwin/amd64", }, {Case: "go.work file", ParseGoWorkFile: true, HasGoWorkFileInParentDir: true, ExpectedString: "1.21"}, { Case: "invalid go.work file fallback", ParseGoWorkFile: true, HasGoWorkFileInParentDir: true, InvalidGoWorkFile: true, ExpectedString: "1.16", Version: "go version go1.16 darwin/amd64", }, { Case: "go.work file with go.mod file uses go.mod's version", ParseModFile: true, HasModFileInParentDir: true, ParseGoWorkFile: true, HasGoWorkFileInParentDir: true, ExpectedString: "1.26.0", }, { Case: "missing both go.mod and go.work file fallback", ParseModFile: true, ParseGoWorkFile: true, ExpectedString: "1.16", Version: "go version go1.16 darwin/amd64", }, } for _, tc := range cases { params := &mockedLanguageParams{ cmd: "go", versionParam: "version", versionOutput: tc.Version, extension: "*.go", } env, props := getMockedLanguageEnv(params) if tc.ParseModFile { props[ParseModFile] = tc.ParseModFile fileInfo := &runtime.FileInfo{ Path: "../go.mod", ParentFolder: "./", IsDir: false, } var err error if !tc.HasModFileInParentDir { err = errors.New("no match") } env.On("HasParentFilePath", "go.mod", false).Return(fileInfo, err) var content string if tc.InvalidModfile { content = "invalid go.mod file" } else { tmp, _ := os.ReadFile(fileInfo.Path) content = string(tmp) } env.On("FileContent", fileInfo.Path).Return(content) } if tc.ParseGoWorkFile { props[ParseWorkFile] = tc.ParseGoWorkFile fileInfo := &runtime.FileInfo{ Path: "../test/go.work", ParentFolder: "./", IsDir: false, } var err error if !tc.HasGoWorkFileInParentDir { err = errors.New("no match") } env.On("HasParentFilePath", "go.work", false).Return(fileInfo, err) var content string if tc.InvalidGoWorkFile { content = "invalid go.work file" } else { tmp, _ := os.ReadFile(fileInfo.Path) content = string(tmp) } env.On("FileContent", fileInfo.Path).Return(content) } g := &Golang{} g.Init(props, env) assert.True(t, g.Enabled(), fmt.Sprintf("Failed in case: %s", tc.Case)) assert.Equal(t, tc.ExpectedString, renderTemplate(env, g.Template(), g), fmt.Sprintf("Failed in case: %s", tc.Case)) } } ================================================ FILE: src/segments/haskell.go ================================================ package segments import "github.com/jandedobbeleer/oh-my-posh/src/segments/options" type Haskell struct { Language StackGhc bool } const ( StackGhcMode options.Option = "stack_ghc_mode" ) func (h *Haskell) Template() string { return languageTemplate } func (h *Haskell) Enabled() bool { ghcRegex := `(?P((?P[0-9]+).(?P[0-9]+).(?P[0-9]+)))` h.extensions = []string{"*.hs", "*.lhs", "stack.yaml", "package.yaml", "*.cabal", "cabal.project"} h.tooling = map[string]*cmd{ "ghc": { executable: "ghc", args: []string{"--numeric-version"}, regex: ghcRegex, }, "stack": { executable: "stack", args: []string{"ghc", "--", "--numeric-version"}, regex: ghcRegex, }, } h.defaultTooling = []string{"ghc"} h.versionURLTemplate = "https://www.haskell.org/ghc/download_ghc_{{ .Major }}_{{ .Minor }}_{{ .Patch }}.html" switch h.options.String(StackGhcMode, "never") { case "always": h.defaultTooling = []string{"stack"} h.StackGhc = true case "package": _, err := h.env.HasParentFilePath("stack.yaml", false) if err == nil { h.defaultTooling = []string{"stack"} h.StackGhc = true } } return h.Language.Enabled() } ================================================ FILE: src/segments/haskell_test.go ================================================ package segments import ( "errors" "fmt" "testing" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/stretchr/testify/assert" ) func TestHaskell(t *testing.T) { cases := []struct { Case string ExpectedString string GhcVersion string StackGhcVersion string StackGhcMode string InStackPackage bool StackGhc bool }{ { Case: "GHC 8.10.7", ExpectedString: "8.10.7", GhcVersion: "8.10.7", StackGhcVersion: "9.0.2", StackGhcMode: "never", }, { Case: "Stack GHC Mode - Always", ExpectedString: "9.0.2", GhcVersion: "8.10.7", StackGhcVersion: "9.0.2", StackGhcMode: "always", StackGhc: true, }, { Case: "Stack GHC Mode - Package", ExpectedString: "9.0.2", GhcVersion: "8.10.7", StackGhcVersion: "9.0.2", StackGhcMode: "package", InStackPackage: true, StackGhc: true, }, { Case: "Stack GHC Mode - Package no stack.yaml", ExpectedString: "8.10.7", GhcVersion: "8.10.7", StackGhcVersion: "9.0.2", StackGhcMode: "package", }, } for _, tc := range cases { params := &mockedLanguageParams{ cmd: "ghc", versionParam: "--numeric-version", versionOutput: tc.GhcVersion, extension: "*.hs", } env, props := getMockedLanguageEnv(params) if tc.StackGhcMode == "always" || (tc.StackGhcMode == "package" && tc.InStackPackage) { env.On("HasCommand", "stack").Return(true) env.On("RunCommand", "stack", []string{"ghc", "--", "--numeric-version"}).Return(tc.StackGhcVersion, nil) } fileInfo := &runtime.FileInfo{ Path: "../stack.yaml", ParentFolder: "./", IsDir: false, } if tc.InStackPackage { var err error env.On("HasParentFilePath", "stack.yaml", false).Return(fileInfo, err) } else { env.On("HasParentFilePath", "stack.yaml", false).Return(fileInfo, errors.New("no match")) } props[StackGhcMode] = tc.StackGhcMode h := &Haskell{} h.Init(props, env) failMsg := fmt.Sprintf("Failed in case: %s", tc.Case) assert.True(t, h.Enabled(), failMsg) assert.Equal(t, tc.ExpectedString, renderTemplate(env, h.Template(), h), failMsg) assert.Equal(t, tc.StackGhc, h.StackGhc, failMsg) } } ================================================ FILE: src/segments/helm.go ================================================ package segments type Helm struct { Base Version string } func (h *Helm) Enabled() bool { displayMode := h.options.String(DisplayMode, DisplayModeAlways) if displayMode != DisplayModeFiles { return h.getVersion() } inChart := false files := []string{"Chart.yml", "Chart.yaml", "helmfile.yaml", "helmfile.yml"} for _, file := range files { if _, err := h.env.HasParentFilePath(file, false); err == nil { inChart = true break } } return inChart && h.getVersion() } func (h *Helm) Template() string { return " Helm {{.Version}}" } func (h *Helm) getVersion() bool { cmd := "helm" if !h.env.HasCommand(cmd) { return false } result, err := h.env.RunCommand(cmd, "version", "--short", "--template={{.Version}}") if err != nil { return false } h.Version = result[1:] return true } ================================================ FILE: src/segments/helm_test.go ================================================ package segments import ( "errors" "testing" "github.com/stretchr/testify/assert" testify_mock "github.com/stretchr/testify/mock" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/runtime/mock" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" ) func TestHelmSegment(t *testing.T) { cases := []struct { Case string ExpectedString string Template string DisplayMode string ChartFile string HelmExists bool ExpectedEnabled bool }{ { Case: "Helm not installed", HelmExists: false, ExpectedEnabled: false, }, { Case: "DisplayMode always inside chart", HelmExists: true, ExpectedEnabled: true, ExpectedString: "Helm 3.12.3", DisplayMode: "always", }, { Case: "DisplayMode always outside chart", HelmExists: true, ExpectedEnabled: true, ExpectedString: "Helm 3.12.3", DisplayMode: "always", }, { Case: "DisplayMode files inside chart. Chart file Chart.yml", HelmExists: true, ExpectedEnabled: true, ExpectedString: "Helm 3.12.3", DisplayMode: "files", ChartFile: "Chart.yml", }, { Case: "DisplayMode always inside chart. Chart file Chart.yaml", HelmExists: true, ExpectedEnabled: true, ExpectedString: "Helm 3.12.3", DisplayMode: "files", ChartFile: "Chart.yaml", }, { Case: "DisplayMode always inside chart. Chart file helmfile.yaml", HelmExists: true, ExpectedEnabled: true, ExpectedString: "Helm 3.12.3", DisplayMode: "files", ChartFile: "helmfile.yaml", }, { Case: "DisplayMode always inside chart. Chart file helmfile.yml", HelmExists: true, ExpectedEnabled: true, ExpectedString: "Helm 3.12.3", DisplayMode: "files", ChartFile: "helmfile.yml", }, { Case: "DisplayMode always outside chart", HelmExists: true, ExpectedEnabled: false, DisplayMode: "files", }, } for _, tc := range cases { env := new(mock.Environment) env.On("HasCommand", "helm").Return(tc.HelmExists) env.On("RunCommand", "helm", []string{"version", "--short", "--template={{.Version}}"}).Return("v3.12.3", nil) env.On("HasParentFilePath", tc.ChartFile, false).Return(&runtime.FileInfo{}, nil) env.On("HasParentFilePath", testify_mock.Anything, false).Return(&runtime.FileInfo{}, errors.New("no such file or directory")) props := options.Map{ DisplayMode: tc.DisplayMode, } h := &Helm{} h.Init(props, env) assert.Equal(t, tc.ExpectedEnabled, h.Enabled(), tc.Case) if tc.ExpectedEnabled { assert.Equal(t, tc.ExpectedString, renderTemplate(env, h.Template(), h), tc.Case) } } } ================================================ FILE: src/segments/http.go ================================================ package segments import ( "encoding/json" "net/http" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" "github.com/jandedobbeleer/oh-my-posh/src/template" ) type HTTP struct { Base Body map[string]any } const ( METHOD options.Option = "method" ) func (h *HTTP) Template() string { return " {{ .Body }} " } func (h *HTTP) Enabled() bool { url := h.options.String(URL, "") if url == "" { return false } method := h.options.String(METHOD, "GET") if resolved, err := template.Render(url, nil); err == nil { url = resolved } result, err := h.getResult(url, method) if err != nil { return false } h.Body = result return true } func (h *HTTP) getResult(url, method string) (map[string]any, error) { setMethod := func(request *http.Request) { request.Method = method } resultBody, err := h.env.HTTPRequest(url, nil, 10000, setMethod) if err != nil { return nil, err } var result map[string]any err = json.Unmarshal(resultBody, &result) if err != nil { return nil, err } return result, nil } ================================================ FILE: src/segments/http_test.go ================================================ package segments import ( "encoding/json" "errors" "testing" "github.com/jandedobbeleer/oh-my-posh/src/runtime/mock" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" "github.com/stretchr/testify/assert" ) func TestHTTPSegmentEnabled(t *testing.T) { cases := []struct { expected any name string url string method string response string shouldError bool }{ { name: "Valid URL with GET response", url: "https://jsonplaceholder.typicode.com/posts/1", method: "GET", response: `{"id": "1"}`, expected: "1", shouldError: false, }, { name: "Valid URL with POST response", url: "https://jsonplaceholder.typicode.com/posts", method: "POST", response: `{"id": "101"}`, expected: "101", shouldError: false, }, { name: "Valid URL with error response", url: "https://api.example.com/data", method: "GET", shouldError: true, }, { name: "Empty URL", url: "", method: "GET", shouldError: false, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { env := new(mock.Environment) props := options.Map{ URL: tc.url, METHOD: tc.method, } env.On("HTTPRequest", tc.url).Return([]byte(tc.response), func() error { if tc.shouldError { return errors.New("error") } return nil }()) cs := &HTTP{ Base: Base{ env: env, options: props, }, } _ = cs.Enabled() assert.Equal(t, tc.expected, cs.Body["id"], tc.name) }) } } func TestHTTPSegmentCache(t *testing.T) { // Simulate what happens when caching response := `{"version": "39.2.6", "count": 42, "enabled": true}` // Create and populate HTTP segment original := &HTTP{ Base: Base{ Segment: &Segment{ Text: " Electron: v39.2.6 ", Index: 1, }, }, } var result map[string]any err := json.Unmarshal([]byte(response), &result) assert.NoError(t, err) original.Body = result // Marshal to JSON (like setCache does) data, err := json.Marshal(original) assert.NoError(t, err) // Unmarshal back (like restoreCache does) restored := &HTTP{ Base: Base{ Segment: &Segment{}, }, } err = json.Unmarshal(data, restored) assert.NoError(t, err) // Verify Body is restored correctly assert.NotNil(t, restored.Body, "Body should not be nil") assert.Equal(t, "39.2.6", restored.Body["version"], "version should be restored") assert.Equal(t, float64(42), restored.Body["count"], "count should be restored") assert.Equal(t, true, restored.Body["enabled"], "enabled should be restored") } ================================================ FILE: src/segments/ipify.go ================================================ package segments import ( "net" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/runtime/http" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" ) type ipData struct { IP string `json:"ip"` } type IPAPI interface { Get() (*ipData, error) } type ipAPI struct { http.Request } func (i *ipAPI) Get() (*ipData, error) { url := "https://api.ipify.org?format=json" return http.Do[*ipData](&i.Request, url, nil) } type IPify struct { Base api IPAPI IP string } const ( OFFLINE = "OFFLINE" ) func (i *IPify) Template() string { return " {{ .IP }} " } func (i *IPify) Enabled() bool { const key = "IP" if ip, ok := cache.Get[string](cache.Device, key); ok { i.IP = ip return true } i.initAPI() ip, err := i.getResult() if err != nil { return false } i.IP = ip duration := i.options.String(options.CacheDuration, string(cache.ONEDAY)) cache.Set(cache.Device, key, i.IP, cache.Duration(duration)) return true } func (i *IPify) getResult() (string, error) { data, err := i.api.Get() if dnsErr, OK := err.(*net.DNSError); OK && dnsErr.IsNotFound { return OFFLINE, nil } if err != nil { return "", err } return data.IP, err } func (i *IPify) initAPI() { if i.api != nil { return } request := &http.Request{ Env: i.env, HTTPTimeout: i.options.Int(options.HTTPTimeout, options.DefaultHTTPTimeout), } i.api = &ipAPI{ Request: *request, } } ================================================ FILE: src/segments/ipify_test.go ================================================ package segments import ( "errors" "net" "testing" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/runtime/mock" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" "github.com/stretchr/testify/assert" testify_ "github.com/stretchr/testify/mock" ) type mockedipAPI struct { testify_.Mock } func (s *mockedipAPI) Get() (*ipData, error) { args := s.Called() return args.Get(0).(*ipData), args.Error(1) } func TestIpifySegment(t *testing.T) { cases := []struct { Case string IPDate *ipData Error error ExpectedString string ExpectedEnabled bool }{ { Case: "IP data", IPDate: &ipData{IP: "127.0.0.1"}, ExpectedString: "127.0.0.1", ExpectedEnabled: true, }, { Case: "Error", Error: errors.New("network is unreachable"), ExpectedEnabled: false, }, { Case: "Offline", ExpectedString: OFFLINE, Error: &net.DNSError{IsNotFound: true}, ExpectedEnabled: true, }, } for _, tc := range cases { api := &mockedipAPI{} api.On("Get").Return(tc.IPDate, tc.Error) ipify := &IPify{ api: api, Base: Base{ env: &mock.Environment{}, options: options.Map{}, }, } enabled := ipify.Enabled() assert.Equal(t, tc.ExpectedEnabled, enabled, tc.Case) cache.DeleteAll(cache.Device) if !enabled { continue } assert.Equal(t, tc.ExpectedString, renderTemplate(&mock.Environment{}, ipify.Template(), ipify), tc.Case) } } ================================================ FILE: src/segments/java.go ================================================ package segments import ( "fmt" ) type Java struct { Language } func (j *Java) Template() string { return languageTemplate } func (j *Java) Enabled() bool { j.init() return j.Language.Enabled() } func (j *Java) init() { javaRegex := `(?: JRE)(?: \(.*\))? \((?P(?P[0-9]+)(?:\.(?P[0-9]+))?(?:\.(?P[0-9]+))?).*\),` j.extensions = []string{ "pom.xml", "build.gradle.kts", "build.sbt", ".java-version", ".deps.edn", "project.clj", "build.boot", "*.java", "*.class", "*.gradle", "*.jar", "*.clj", "*.cljc", } j.tooling = map[string]*cmd{ "java": { executable: "java", args: []string{"-Xinternalversion"}, regex: javaRegex, }, } j.defaultTooling = []string{"java"} javaHome := j.env.Getenv("JAVA_HOME") if len(javaHome) > 0 { java := fmt.Sprintf("%s/bin/java", javaHome) j.tooling["java_home"] = &cmd{ executable: java, args: []string{"-Xinternalversion"}, regex: javaRegex, } j.defaultTooling = []string{"java_home", "java"} } } ================================================ FILE: src/segments/java_test.go ================================================ package segments import ( "fmt" "testing" "github.com/stretchr/testify/assert" ) func TestJava(t *testing.T) { cases := []struct { Case string ExpectedString string Version string JavaHomeVersion string JavaHomeEnabled bool }{ { Case: "Zulu LTS", ExpectedString: "11.0.13", Version: "OpenJDK 64-Bit Server VM (11.0.13+8-LTS) for windows-amd64 JRE (Zulu11.52+13-CA) (11.0.13+8-LTS), built on Oct 7 2021 16:00:23 by \"zulu_re\" with MS VC++ 15.9 (VS2017)", //nolint:lll }, { Case: "OpenJDK macOS", ExpectedString: "1.8.0", Version: "OpenJDK 64-Bit Server VM (25.275-b01) for bsd-amd64 JRE (1.8.0_275-b01), built on Nov 9 2020 12:07:35 by \"jenkins\" with gcc 4.2.1", }, { Case: "OpenJDK macOS with JAVA_HOME, no executable", ExpectedString: "1.8.0", Version: "OpenJDK 64-Bit Server VM (25.275-b01) for bsd-amd64 JRE (1.8.0_275-b01), built on Nov 9 2020 12:07:35 by \"jenkins\" with gcc 4.2.1", }, { Case: "OpenJDK macOS with JAVA_HOME and executable", ExpectedString: "1.7.0", JavaHomeEnabled: true, JavaHomeVersion: "OpenJDK 64-Bit Server VM (25.275-b01) for bsd-amd64 JRE (1.7.0_275-b01), built on Nov 9 2020 12:07:35 by \"jenkins\" with gcc 4.2.1", Version: "OpenJDK 64-Bit Server VM (25.275-b01) for bsd-amd64 JRE (1.8.0_275-b01), built on Nov 9 2020 12:07:35 by \"jenkins\" with gcc 4.2.1", }, { Case: "openjdk version \"15.0.2\" 2021-01-19", ExpectedString: "15.0.2", JavaHomeEnabled: true, JavaHomeVersion: "OpenJDK 64-Bit Server VM (15.0.2+7) for windows-amd64 JRE (15.0.2+7), built on Jan 21 2021 05:54:57 by \"\" with MS VC++ 15.9 (VS2017)", Version: "OpenJDK 64-Bit Server VM (15.0.2+7) for windows-amd64 JRE (15.0.2+7), built on Jan 21 2021 05:54:57 by \"\" with MS VC++ 15.9 (VS2017)", }, { Case: "openjdk version \"16\" 2021-03-16", ExpectedString: "16", JavaHomeEnabled: true, JavaHomeVersion: "OpenJDK 64-Bit Server VM (16+36) for windows-amd64 JRE (16+36), built on Mar 11 2021 10:56:33 by \"\" with MS VC++ 16.7 (VS2019)", Version: "OpenJDK 64-Bit Server VM (16+36) for windows-amd64 JRE (16+36), built on Mar 11 2021 10:56:33 by \"\" with MS VC++ 16.7 (VS2019)", }, } for _, tc := range cases { params := &mockedLanguageParams{ cmd: "java", versionParam: "-Xinternalversion", versionOutput: tc.Version, extension: "pom.xml", } env, props := getMockedLanguageEnv(params) if tc.JavaHomeEnabled { env.On("Getenv", "JAVA_HOME").Return("/usr/java") env.On("HasCommand", "/usr/java/bin/java").Return(true) env.On("RunCommand", "/usr/java/bin/java", []string{"-Xinternalversion"}).Return(tc.JavaHomeVersion, nil) } else { env.On("Getenv", "JAVA_HOME").Return("") } j := &Java{} j.Init(props, env) assert.True(t, j.Enabled(), fmt.Sprintf("Failed in case: %s", tc.Case)) assert.Equal(t, tc.ExpectedString, renderTemplate(env, j.Template(), j), fmt.Sprintf("Failed in case: %s", tc.Case)) } } ================================================ FILE: src/segments/jujutsu.go ================================================ package segments import ( "fmt" "strconv" "strings" "github.com/jandedobbeleer/oh-my-posh/src/log" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/runtime/path" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" ) const ( JUJUTSUCOMMAND = "jj" IgnoreWorkingCopy options.Option = "ignore_working_copy" ChangeIDMinLen options.Option = "change_id_min_len" FetchAhead options.Option = "fetch_ahead_counter" AheadIcon options.Option = "ahead_icon" ) type JujutsuStatus struct { ScmStatus } func (s *JujutsuStatus) add(code byte) { switch code { case 'D': s.Deleted++ case 'A', 'C': // added, copied s.Added++ case 'M': s.Modified++ case 'R': // renamed s.Moved++ } } type Jujutsu struct { Working *JujutsuStatus ChangeID string Scm } func (jj *Jujutsu) Template() string { return " \uf1fa{{.ChangeID}}{{if .Working.Changed}} \uf044 {{ .Working.String }}{{ end }} " } func (jj *Jujutsu) Enabled() bool { displayStatus := jj.options.Bool(FetchStatus, false) if !jj.shouldDisplay(displayStatus) { return false } statusFormats := jj.options.KeyValueMap(StatusFormats, map[string]string{}) jj.Working = &JujutsuStatus{ScmStatus: ScmStatus{Formats: statusFormats}} if displayStatus { jj.setJujutsuStatus() } return true } func (jj *Jujutsu) CacheKey() (string, bool) { dir, err := jj.env.HasParentFilePath(".jj", true) if err != nil { return "", false } return dir.Path, true } func (jj *Jujutsu) ClosestBookmarks() string { statusString, err := jj.getJujutsuCommandOutput("log", "-r", "heads(::@ & bookmarks())", "--no-graph", "-T", "bookmarks") if err != nil { return "" } line, _, _ := strings.Cut(statusString, "\n") if !jj.options.Bool(FetchAhead, false) || len(line) == 0 { return line } aheadIcon := jj.options.String(AheadIcon, "\u21e1") marks := strings.Split(line, " ") // String to return for status var endString strings.Builder // Closest bookmarks are all the same distance away from the working copy // so retrieve the distance to the first one and use it for all of them rangeString := strings.Trim(marks[0], "*") + "..@" aheadString, err := jj.getJujutsuCommandOutput("log", "--no-graph", "-T", "'.'", "-r", rangeString) if err != nil { return line } aheadCounter := len(aheadString) aheadCounterString := "" if aheadCounter != 0 { aheadCounterString = aheadIcon + strconv.Itoa(aheadCounter) } log.Debug("distance to nearest jj bookmark:" + aheadCounterString) // Loop through each bookmark for index, mark := range marks { if index > 0 { endString.WriteString(" ") } endString.WriteString(mark + aheadCounterString) } return endString.String() } func (jj *Jujutsu) shouldDisplay(displayStatus bool) bool { jjdir, err := jj.env.HasParentFilePath(".jj", false) if err != nil { log.Debug("Jujutsu directory not found") return false } if displayStatus && !jj.hasCommand(JUJUTSUCOMMAND) { log.Debug("Jujutsu command not found, skipping segment") return false } jj.setDir(jjdir.ParentFolder) jj.mainSCMDir = jjdir.Path jj.scmDir = jjdir.Path // convert the worktree file path to a windows one when in a WSL shared folder jj.repoRootDir = strings.TrimSuffix(jj.convertToWindowsPath(jjdir.Path), "/.jj") return true } func (jj *Jujutsu) setDir(dir string) { dir = path.ReplaceHomeDirPrefixWithTilde(dir) // align with template PWD if jj.env.GOOS() == runtime.WINDOWS { jj.Dir = strings.TrimSuffix(dir, `\.jj`) return } jj.Dir = strings.TrimSuffix(dir, "/.jj") } func (jj *Jujutsu) setJujutsuStatus() { statusString, err := jj.getJujutsuCommandOutput("log", "-r", "@", "--no-graph", "-T", jj.logTemplate()) if err != nil { return } lines := strings.Split(statusString, "\n") jj.ChangeID = lines[0] for _, line := range lines[1:] { if len(line) > 0 { jj.Working.add(line[0]) } } } func (jj *Jujutsu) logTemplate() string { // https://jj-vcs.github.io/jj/latest/templates/#commit-keywords return fmt.Sprintf(`change_id.shortest(%d) ++ "\n" ++ diff.summary()`, jj.options.Int(ChangeIDMinLen, 0)) } func (jj *Jujutsu) getJujutsuCommandOutput(command string, args ...string) (string, error) { cli := []string{"--repository", jj.repoRootDir, "--no-pager", "--color", "never"} if jj.options.Bool(IgnoreWorkingCopy, true) { cli = append(cli, "--ignore-working-copy") } cli = append(cli, command) cli = append(cli, args...) return jj.env.RunCommand(jj.command, cli...) } ================================================ FILE: src/segments/jujutsu_test.go ================================================ package segments import ( "errors" "testing" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/runtime/mock" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" "github.com/stretchr/testify/assert" ) func TestJujutsuEnabledToolNotFound(t *testing.T) { env := new(mock.Environment) env.On("InWSLSharedDrive").Return(false) env.On("HasParentFilePath", ".jj", false).Return(&runtime.FileInfo{}, errors.New("not found")) env.On("GOOS").Return("") env.On("IsWsl").Return(false) jj := &Jujutsu{} jj.Init(options.Map{}, env) assert.False(t, jj.Enabled()) } func TestJujutsuEnabledInWorkingDirectory(t *testing.T) { fileInfo := &runtime.FileInfo{ Path: "/dir/hello", ParentFolder: "/dir", IsDir: true, } env := new(mock.Environment) env.On("InWSLSharedDrive").Return(false) env.On("HasCommand", "jj").Return(true) env.On("HasParentFilePath", ".jj", false).Return(fileInfo, nil) env.On("GOOS").Return("") jj := &Jujutsu{} jj.Init(options.Map{}, env) assert.True(t, jj.Enabled()) assert.Equal(t, fileInfo.Path, jj.mainSCMDir) assert.Equal(t, fileInfo.Path, jj.repoRootDir) } func TestJujutsuGetIdInfo(t *testing.T) { cases := []struct { ExpectedWorking *JujutsuStatus Case string LogOutput string ExpectedChangeID string }{ { Case: "nochanges", LogOutput: "a\n\n", ExpectedChangeID: "a", ExpectedWorking: &JujutsuStatus{ScmStatus{ Deleted: 0, Added: 0, Modified: 0, Moved: 0, }}, }, { Case: "changed", LogOutput: `b D deleted_file A added_file C {copied_file => new_file} M modified_file R {renamed_file => new_file} `, ExpectedChangeID: "b", ExpectedWorking: &JujutsuStatus{ScmStatus{ Deleted: 1, Added: 2, Modified: 1, Moved: 1, }}, }, } for _, tc := range cases { fileInfo := &runtime.FileInfo{ Path: "/dir/hello", ParentFolder: "/dir", IsDir: true, } props := options.Map{ FetchStatus: true, } env := new(mock.Environment) env.On("InWSLSharedDrive").Return(false) env.On("HasCommand", "jj").Return(true) env.On("GOOS").Return("") env.On("IsWsl").Return(false) env.On("HasParentFilePath", ".jj", false).Return(fileInfo, nil) env.On("PathSeparator").Return("/") env.On("Home").Return(poshHome) env.On("Getenv", poshGitEnv).Return("") jj := &Jujutsu{} jj.Init(props, env) env.MockJjCommand(fileInfo.Path, tc.LogOutput, "log", "-r", "@", "--no-graph", "-T", jj.logTemplate()) if tc.ExpectedWorking != nil { tc.ExpectedWorking.Formats = map[string]string{} } assert.True(t, jj.Enabled()) assert.Equal(t, fileInfo.Path, jj.mainSCMDir) assert.Equal(t, fileInfo.Path, jj.repoRootDir) assert.Equal(t, tc.ExpectedWorking, jj.Working, tc.Case) assert.Equal(t, tc.ExpectedChangeID, jj.ChangeID, tc.Case) } } ================================================ FILE: src/segments/julia.go ================================================ package segments type Julia struct { Language } func (j *Julia) Template() string { return languageTemplate } func (j *Julia) Enabled() bool { j.extensions = []string{"*.jl"} j.tooling = map[string]*cmd{ "julia": { executable: "julia", args: []string{"--version"}, regex: `julia version (?P((?P[0-9]+).(?P[0-9]+).(?P[0-9]+)))`, }, } j.defaultTooling = []string{"julia"} j.versionURLTemplate = "https://github.com/JuliaLang/julia/releases/tag/v{{ .Full }}" return j.Language.Enabled() } ================================================ FILE: src/segments/julia_test.go ================================================ package segments import ( "fmt" "testing" "github.com/stretchr/testify/assert" ) func TestJulia(t *testing.T) { cases := []struct { Case string ExpectedString string Version string }{ {Case: "Julia 1.6.0", ExpectedString: "1.6.0", Version: "julia version 1.6.0"}, {Case: "Julia 1.6.1", ExpectedString: "1.6.1", Version: "julia version 1.6.1"}, } for _, tc := range cases { params := &mockedLanguageParams{ cmd: "julia", versionParam: "--version", versionOutput: tc.Version, extension: "*.jl", } env, props := getMockedLanguageEnv(params) j := &Julia{} j.Init(props, env) assert.True(t, j.Enabled(), fmt.Sprintf("Failed in case: %s", tc.Case)) assert.Equal(t, tc.ExpectedString, renderTemplate(env, j.Template(), j), fmt.Sprintf("Failed in case: %s", tc.Case)) } } ================================================ FILE: src/segments/kotlin.go ================================================ package segments type Kotlin struct { Language } func (k *Kotlin) Template() string { return languageTemplate } func (k *Kotlin) Enabled() bool { k.extensions = []string{"*.kt", "*.kts", "*.ktm"} k.tooling = map[string]*cmd{ "kotlin": { executable: "kotlin", args: []string{"-version"}, regex: `Kotlin version (?P((?P[0-9]+).(?P[0-9]+).(?P[0-9]+)))`, }, } k.defaultTooling = []string{"kotlin"} k.versionURLTemplate = "https://github.com/JetBrains/kotlin/releases/tag/v{{ .Full }}" return k.Language.Enabled() } ================================================ FILE: src/segments/kotlin_test.go ================================================ package segments import ( "fmt" "testing" "github.com/stretchr/testify/assert" ) func TestKotlin(t *testing.T) { cases := []struct { Case string ExpectedString string Version string }{ {Case: "Kotlin 1.6.10", ExpectedString: "1.6.10", Version: "Kotlin version 1.6.10-release-923 (JRE 17.0.2+0)"}, {Case: "Kotlin 1.6.0", ExpectedString: "1.6.0", Version: "Kotlin version 1.6.0-release-915 (JRE 17.0.2+0)"}, } for _, tc := range cases { params := &mockedLanguageParams{ cmd: "kotlin", versionParam: "-version", versionOutput: tc.Version, extension: "*.kt", } env, props := getMockedLanguageEnv(params) k := &Kotlin{} k.Init(props, env) assert.True(t, k.Enabled(), fmt.Sprintf("Failed in case: %s", tc.Case)) assert.Equal(t, tc.ExpectedString, renderTemplate(env, k.Template(), k), fmt.Sprintf("Failed in case: %s", tc.Case)) } } ================================================ FILE: src/segments/kubectl.go ================================================ package segments import ( "path/filepath" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" yaml "go.yaml.in/yaml/v3" ) // Whether to use kubectl or read kubeconfig ourselves const ( ParseKubeConfig options.Option = "parse_kubeconfig" ContextAliases options.Option = "context_aliases" ClusterAliases options.Option = "cluster_aliases" ) type Kubectl struct { Base KubeContext Context string dirty bool } type KubeConfig struct { CurrentContext string `yaml:"current-context"` Contexts []struct { Context *KubeContext `yaml:"context"` Name string `yaml:"name"` } `yaml:"contexts"` } type KubeContext struct { Cluster string `yaml:"cluster"` User string `yaml:"user"` Namespace string `yaml:"namespace"` } func (k *Kubectl) Template() string { return " {{ .Context }}{{ if .Namespace }} :: {{ .Namespace }}{{ end }} " } func (k *Kubectl) Enabled() bool { parseKubeConfig := k.options.Bool(ParseKubeConfig, true) if parseKubeConfig { return k.doParseKubeConfig() } return k.doCallKubectl() } func (k *Kubectl) doParseKubeConfig() bool { // Follow kubectl search rules (see https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#the-kubeconfig-environment-variable) // TL;DR: KUBECONFIG can contain a list of files. If it's empty ~/.kube/config is used. First file in list wins when merging keys. kubeconfigs := filepath.SplitList(k.env.Getenv("KUBECONFIG")) if len(kubeconfigs) == 0 { kubeconfigs = []string{filepath.Join(k.env.Home(), ".kube/config")} } contexts := make(map[string]*KubeContext) k.Context = "" for _, kubeconfig := range kubeconfigs { if kubeconfig == "" { continue } content := k.env.FileContent(kubeconfig) var config KubeConfig err := yaml.Unmarshal([]byte(content), &config) if err != nil { continue } for _, context := range config.Contexts { if _, exists := contexts[context.Name]; !exists { contexts[context.Name] = context.Context } } if k.Context == "" { k.Context = config.CurrentContext } context, exists := contexts[k.Context] if !exists { continue } if context != nil { k.KubeContext = *context } k.SetContextAlias() k.SetClusterAlias() k.dirty = true return true } displayError := k.options.Bool(options.DisplayError, false) if !displayError { return false } k.setError("KUBECONFIG ERR") return true } func (k *Kubectl) doCallKubectl() bool { cmd := "kubectl" if !k.env.HasCommand(cmd) { return false } result, err := k.env.RunCommand(cmd, "config", "view", "--output", "yaml", "--minify") displayError := k.options.Bool(options.DisplayError, false) if err != nil && displayError { k.setError("KUBECTL ERR") return true } if err != nil { return false } var config KubeConfig err = yaml.Unmarshal([]byte(result), &config) if err != nil { return false } k.Context = config.CurrentContext k.SetContextAlias() k.dirty = true if len(config.Contexts) > 0 { k.KubeContext = *config.Contexts[0].Context k.SetClusterAlias() } return true } func (k *Kubectl) setError(message string) { if k.Context == "" { k.Context = message } k.Namespace = message k.User = message k.Cluster = message } func (k *Kubectl) SetContextAlias() { aliases := k.options.KeyValueMap(ContextAliases, map[string]string{}) if alias, exists := aliases[k.Context]; exists { k.Context = alias } } func (k *Kubectl) SetClusterAlias() { aliases := k.options.KeyValueMap(ClusterAliases, map[string]string{}) if alias, exists := aliases[k.Cluster]; exists { k.Cluster = alias } } ================================================ FILE: src/segments/kubectl_test.go ================================================ package segments import ( "fmt" "os" "path/filepath" "testing" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/runtime/mock" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" "github.com/stretchr/testify/assert" ) const ( testKubectlAllInfoTemplate = "{{.Context}} :: {{.Namespace}} :: {{.User}} :: {{.Cluster}}" contextMarker = "currentcontextmarker" ) func TestKubectlSegment(t *testing.T) { standardTemplate := "{{.Context}}{{if .Namespace}} :: {{.Namespace}}{{end}}" lsep := string(filepath.ListSeparator) cases := []struct { Files map[string]string ContextAliases map[string]string ClusterAliases map[string]string Cluster string Kubeconfig string Context string Namespace string UserName string Case string ExpectedString string Template string KubectlExists bool ParseKubeConfig bool KubectlErr bool ExpectedEnabled bool DisplayError bool }{ { Case: "kubeconfig incomplete", Template: testKubectlAllInfoTemplate, ParseKubeConfig: true, Kubeconfig: contextMarker + lsep + "contextdefinitionincomplete", Files: testKubeConfigFiles, ExpectedString: "ctx :: :: ::", ExpectedEnabled: true, }, {Case: "disabled", Template: standardTemplate, KubectlExists: false, Context: "aaa", Namespace: "bbb", ExpectedEnabled: false}, { Case: "all information", Template: testKubectlAllInfoTemplate, KubectlExists: true, Context: "aaa", Namespace: "bbb", UserName: "ccc", Cluster: "ddd", ExpectedString: "aaa :: bbb :: ccc :: ddd", ExpectedEnabled: true, }, {Case: "no namespace", Template: standardTemplate, KubectlExists: true, Context: "aaa", ExpectedString: "aaa", ExpectedEnabled: true}, { Case: "kubectl context alias", Template: standardTemplate, KubectlExists: true, Context: "aaa", Namespace: "bbb", ContextAliases: map[string]string{"aaa": "ccc"}, ExpectedString: "ccc :: bbb", ExpectedEnabled: true, }, { Case: "kubectl error", Template: standardTemplate, DisplayError: true, KubectlExists: true, Context: "aaa", Namespace: "bbb", KubectlErr: true, ExpectedString: "KUBECTL ERR :: KUBECTL ERR", ExpectedEnabled: true, }, {Case: "kubectl error hidden", Template: standardTemplate, DisplayError: false, KubectlExists: true, Context: "aaa", Namespace: "bbb", KubectlErr: true, ExpectedEnabled: false}, { Case: "kubeconfig home", Template: testKubectlAllInfoTemplate, ParseKubeConfig: true, Files: testKubeConfigFiles, ExpectedString: "aaa :: bbb :: ccc :: ddd", ExpectedEnabled: true, }, { Case: "kubeconfig context alias", Template: standardTemplate, ParseKubeConfig: true, Files: testKubeConfigFiles, ContextAliases: map[string]string{"aaa": "ccc"}, ExpectedString: "ccc :: bbb", ExpectedEnabled: true, }, { Case: "kubectl cluster alias", Template: testKubectlAllInfoTemplate, KubectlExists: true, Context: "aaa", Namespace: "bbb", UserName: "ccc", Cluster: "ddd", ClusterAliases: map[string]string{"ddd": "production"}, ExpectedString: "aaa :: bbb :: ccc :: production", ExpectedEnabled: true, }, { Case: "kubeconfig cluster alias", Template: testKubectlAllInfoTemplate, ParseKubeConfig: true, Files: testKubeConfigFiles, ClusterAliases: map[string]string{"ddd": "prod"}, ExpectedString: "aaa :: bbb :: ccc :: prod", ExpectedEnabled: true, }, { Case: "kubeconfig context and cluster alias", Template: testKubectlAllInfoTemplate, ParseKubeConfig: true, Files: testKubeConfigFiles, ContextAliases: map[string]string{"aaa": "my-context"}, ClusterAliases: map[string]string{"ddd": "my-cluster"}, ExpectedString: "my-context :: bbb :: ccc :: my-cluster", ExpectedEnabled: true, }, { Case: "kubeconfig multiple current marker first", Template: testKubectlAllInfoTemplate, ParseKubeConfig: true, Kubeconfig: "" + lsep + contextMarker + lsep + "contextdefinition" + lsep + "contextredefinition", Files: testKubeConfigFiles, ExpectedString: "ctx :: ns :: usr :: cl", ExpectedEnabled: true, }, { Case: "kubeconfig multiple context first", Template: testKubectlAllInfoTemplate, ParseKubeConfig: true, Kubeconfig: "contextdefinition" + lsep + "contextredefinition" + lsep + contextMarker + lsep, Files: testKubeConfigFiles, ExpectedString: "ctx :: ns :: usr :: cl", ExpectedEnabled: true, }, { Case: "kubeconfig error hidden", Template: testKubectlAllInfoTemplate, ParseKubeConfig: true, Kubeconfig: "invalid", Files: testKubeConfigFiles, ExpectedEnabled: false}, { Case: "kubeconfig error", Template: testKubectlAllInfoTemplate, ParseKubeConfig: true, Kubeconfig: "invalid", Files: testKubeConfigFiles, DisplayError: true, ExpectedString: "KUBECONFIG ERR :: KUBECONFIG ERR :: KUBECONFIG ERR :: KUBECONFIG ERR", ExpectedEnabled: true, }, } for _, tc := range cases { env := new(mock.Environment) env.On("HasCommand", "kubectl").Return(tc.KubectlExists) var kubeconfig string content, err := os.ReadFile("../test/kubectl.yml") if err == nil { kubeconfig = fmt.Sprintf(string(content), tc.Cluster, tc.UserName, tc.Namespace, tc.Context) } var kubectlErr error if tc.KubectlErr { kubectlErr = &runtime.CommandError{ Err: "oops", ExitCode: 1, } } env.On("RunCommand", "kubectl", []string{"config", "view", "--output", "yaml", "--minify"}).Return(kubeconfig, kubectlErr) env.On("Getenv", "KUBECONFIG").Return(tc.Kubeconfig) for path, content := range tc.Files { env.On("FileContent", path).Return(content) } env.On("Home").Return("testhome") props := options.Map{ options.DisplayError: tc.DisplayError, ParseKubeConfig: tc.ParseKubeConfig, ContextAliases: tc.ContextAliases, ClusterAliases: tc.ClusterAliases, } k := &Kubectl{} k.Init(props, env) assert.Equal(t, tc.ExpectedEnabled, k.Enabled(), tc.Case) if tc.ExpectedEnabled { assert.Equal(t, tc.ExpectedString, renderTemplate(env, tc.Template, k), tc.Case) } } } var testKubeConfigFiles = map[string]string{ filepath.Join("testhome", ".kube/config"): ` apiVersion: v1 contexts: - context: cluster: ddd user: ccc namespace: bbb name: aaa current-context: aaa `, "contextdefinition": ` apiVersion: v1 contexts: - context: cluster: cl user: usr namespace: ns name: ctx `, contextMarker: ` apiVersion: v1 current-context: ctx `, "invalid": "this is not yaml", "contextdefinitionincomplete": ` apiVersion: v1 contexts: - name: ctx `, "contextredefinition": ` apiVersion: v1 contexts: - context: cluster: wrongcl user: wrongu namespace: wrongns name: ctx `, } ================================================ FILE: src/segments/language.go ================================================ package segments import ( "encoding/json" "errors" "fmt" "path/filepath" runtime_ "runtime" "slices" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/log" "github.com/jandedobbeleer/oh-my-posh/src/regex" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" "github.com/jandedobbeleer/oh-my-posh/src/template" ) const ( languageTemplate = " {{ if .Error }}{{ .Error }}{{ else }}{{ .Full }}{{ end }} " noVersion = "NO VERSION" ) type loadContext func() type inContext func() bool type getVersion func() (string, error) type matchesVersionFile func() (string, bool) type Version struct { Full string Major string Minor string Patch string Prerelease string BuildMetadata string URL string Executable string Expected string } type cmd struct { getVersion getVersion executable string regex string versionURLTemplate string args []string } func (c *cmd) parse(versionInfo string) (*Version, error) { values := regex.FindNamedRegexMatch(c.regex, versionInfo) if len(values) == 0 { return nil, errors.New("cannot parse version string") } version := &Version{ Full: values["version"], Major: values["major"], Minor: values["minor"], Patch: values["patch"], Prerelease: values["prerelease"], BuildMetadata: values["buildmetadata"], } return version, nil } type Language struct { Base projectRoot *runtime.FileInfo loadContext loadContext inContext inContext matchesVersionFile matchesVersionFile Version displayMode string Error string versionURLTemplate string name string commands []*cmd tooling map[string]*cmd defaultTooling []string projectFiles []string folders []string extensions []string exitCode int homeEnabled bool Mismatch bool } const ( // DisplayMode sets the display mode (always, when_in_context, never) DisplayMode options.Option = "display_mode" // DisplayModeAlways displays the segment always DisplayModeAlways string = "always" // DisplayModeFiles displays the segment when the current folder contains certain extensions DisplayModeFiles string = "files" // DisplayModeEnvironment displays the segment when the environment has a language's context DisplayModeEnvironment string = "environment" // DisplayModeContext displays the segment when the environment or files is active DisplayModeContext string = "context" // MissingCommandText sets the text to display when the command is not present in the system MissingCommandText options.Option = "missing_command_text" // HomeEnabled displays the segment in the HOME folder or not HomeEnabled options.Option = "home_enabled" // LanguageExtensions the list of extensions to validate LanguageExtensions options.Option = "extensions" // LanguageFolders the list of folders to validate LanguageFolders options.Option = "folders" // Tooling allows enabling additional version fetching tools Tooling options.Option = "tooling" ) func (l *Language) getName() string { _, file, _, _ := runtime_.Caller(2) base := filepath.Base(file) return base[:len(base)-3] } func (l *Language) Enabled() bool { l.name = l.getName() // override default extensions if needed l.extensions = l.options.StringArray(LanguageExtensions, l.extensions) l.folders = l.options.StringArray(LanguageFolders, l.folders) inHomeDir := func() bool { return l.env.Pwd() == l.env.Home() } var enabled bool homeEnabled := l.options.Bool(HomeEnabled, l.homeEnabled) if inHomeDir() && !homeEnabled { return false } if len(l.projectFiles) != 0 && l.hasProjectFiles() { enabled = true } if !enabled { // set default mode when not set if l.displayMode == "" { l.displayMode = l.options.String(DisplayMode, DisplayModeFiles) } l.loadLanguageContext() switch l.displayMode { case DisplayModeAlways: enabled = true case DisplayModeEnvironment: enabled = l.inLanguageContext() case DisplayModeFiles: enabled = l.hasLanguageFiles() || l.hasLanguageFolders() case DisplayModeContext: fallthrough default: enabled = l.hasLanguageFiles() || l.hasLanguageFolders() || l.inLanguageContext() || l.hasProjectFiles() } } l.loadTooling() if !enabled || !l.options.Bool(options.FetchVersion, true) { return enabled } err := l.setVersion() if err != nil { l.Error = err.Error() } if l.matchesVersionFile != nil { expected, match := l.matchesVersionFile() if !match { l.Mismatch = true l.Expected = expected } } return enabled } // loadTooling builds the commands list from the tooling map based on the tooling configuration. // Users can override the default tooling via the Tooling option. // This allows specifying which tools should be used to fetch versions // (e.g., "uv" for Python to use UV package manager). func (l *Language) loadTooling() { enabledTools := l.options.StringArray(Tooling, l.defaultTooling) if len(enabledTools) == 0 { return } var commands []*cmd for _, tool := range enabledTools { if command, exists := l.tooling[tool]; exists { commands = append(commands, command) } } l.commands = commands } func (l *Language) hasLanguageFiles() bool { return slices.ContainsFunc(l.extensions, l.env.HasFiles) } func (l *Language) hasProjectFiles() bool { for _, extension := range l.projectFiles { if configPath, err := l.env.HasParentFilePath(extension, false); err == nil { l.projectRoot = configPath return true } } return false } func (l *Language) hasLanguageFolders() bool { return slices.ContainsFunc(l.folders, l.env.HasFolder) } // setVersion parses the version string returned by the command func (l *Language) setVersion() error { var lastError error cacheKey := fmt.Sprintf("version_%s", l.name) if versionCache, OK := cache.Get[Version](cache.Device, cacheKey); OK { l.Version = versionCache return nil } for _, command := range l.commands { versionStr, err := l.runCommand(command) if err != nil { log.Error(err) lastError = err continue } version, err := command.parse(versionStr) if err != nil { log.Error(err) lastError = fmt.Errorf("err parsing info from %s with %s", command.executable, versionStr) continue } l.Version = *version if command.versionURLTemplate != "" { l.versionURLTemplate = command.versionURLTemplate } l.buildVersionURL() l.Executable = command.executable duration := l.options.String(options.CacheDuration, string(cache.NONE)) cache.Set(cache.Device, cacheKey, l.Version, cache.Duration(duration)) return nil } if lastError != nil { return lastError } return errors.New(l.options.String(MissingCommandText, "")) } func (l *Language) runCommand(command *cmd) (string, error) { if command.getVersion == nil { if !l.env.HasCommand(command.executable) { return "", errors.New(noVersion) } versionStr, err := l.env.RunCommand(command.executable, command.args...) if exitErr, ok := err.(*runtime.CommandError); ok { l.exitCode = exitErr.ExitCode return "", fmt.Errorf("err executing %s with %s", command.executable, command.args) } return versionStr, nil } versionStr, err := command.getVersion() if err != nil { return "", err } if versionStr == "" { return "", errors.New("no version found") } return versionStr, nil } func (l *Language) loadLanguageContext() { if l.loadContext == nil { return } l.loadContext() } func (l *Language) inLanguageContext() bool { if l.inContext == nil { return false } return l.inContext() } func (l *Language) buildVersionURL() { versionURLTemplate := l.options.String(options.VersionURLTemplate, l.versionURLTemplate) if versionURLTemplate == "" { return } url, err := template.Render(versionURLTemplate, l.Version) if err != nil { return } l.URL = url } func (l *Language) hasNodePackage(name string) bool { packageJSON := l.env.FileContent("package.json") var packageData map[string]any if err := json.Unmarshal([]byte(packageJSON), &packageData); err != nil { return false } dependencies, ok := packageData["dependencies"].(map[string]any) if !ok { return false } if _, exists := dependencies[name]; !exists { return false } return true } func (l *Language) nodePackageVersion(name string) (string, error) { folder := filepath.Join(l.env.Pwd(), "node_modules", name) const fileName string = "package.json" if !l.env.HasFilesInDir(folder, fileName) { return "", fmt.Errorf("%s not found in %s", fileName, folder) } content := l.env.FileContent(filepath.Join(folder, fileName)) var data ProjectData err := json.Unmarshal([]byte(content), &data) if err != nil { return "", err } return data.Version, nil } ================================================ FILE: src/segments/language_test.go ================================================ package segments import ( "path/filepath" "slices" "testing" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/runtime/mock" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" "github.com/stretchr/testify/assert" ) const ( universion = "1.3.307" uni = "*.uni" corn = "*.corn" ) type languageArgs struct { expectedError error options options.Provider matchesVersionFile matchesVersionFile version string versionURLTemplate string extensions []string enabledExtensions []string commands []*cmd enabledCommands []string inHome bool } func (l *languageArgs) hasvalue(value string, list []string) bool { return slices.Contains(list, value) } func bootStrapLanguageTest(args *languageArgs) *Language { env := new(mock.Environment) for _, command := range args.commands { env.On("HasCommand", command.executable).Return(args.hasvalue(command.executable, args.enabledCommands)) env.On("RunCommand", command.executable, command.args).Return(args.version, args.expectedError) } for _, extension := range args.extensions { env.On("HasFiles", extension).Return(args.hasvalue(extension, args.enabledExtensions)) } home := "/usr/home" cwd := "/usr/home/project" if args.inHome { cwd = home } env.On("Pwd").Return(cwd) env.On("Home").Return(home) if args.options == nil { args.options = options.Map{} } l := &Language{ extensions: args.extensions, commands: args.commands, versionURLTemplate: args.versionURLTemplate, matchesVersionFile: args.matchesVersionFile, } l.Init(args.options, env) return l } func TestLanguageFilesFoundButNoCommandAndVersionAndDisplayVersion(t *testing.T) { args := &languageArgs{ commands: []*cmd{ { executable: "unicorn", args: []string{"--version"}, }, }, extensions: []string{uni}, enabledExtensions: []string{uni}, } lang := bootStrapLanguageTest(args) assert.True(t, lang.Enabled()) assert.Equal(t, noVersion, lang.Error, "unicorn is not available") } func TestLanguageFilesFoundButNoCommandAndVersionAndDontDisplayVersion(t *testing.T) { props := options.Map{ options.FetchVersion: false, } args := &languageArgs{ commands: []*cmd{ { executable: "unicorn", args: []string{"--version"}, }, }, extensions: []string{uni}, enabledExtensions: []string{uni}, options: props, } lang := bootStrapLanguageTest(args) assert.True(t, lang.Enabled(), "unicorn is not available") } func TestLanguageFilesFoundButNoCommandAndNoVersion(t *testing.T) { args := &languageArgs{ commands: []*cmd{ { executable: "unicorn", args: []string{"--version"}, }, }, extensions: []string{uni}, enabledExtensions: []string{uni}, } lang := bootStrapLanguageTest(args) assert.True(t, lang.Enabled(), "unicorn is not available") } func TestLanguageDisabledNoFiles(t *testing.T) { args := &languageArgs{ commands: []*cmd{ { executable: "unicorn", args: []string{"--version"}, }, }, extensions: []string{uni}, enabledExtensions: []string{}, enabledCommands: []string{"unicorn"}, } lang := bootStrapLanguageTest(args) assert.False(t, lang.Enabled(), "no files in the current directory") } func TestLanguageEnabledOneExtensionFound(t *testing.T) { args := &languageArgs{ commands: []*cmd{ { executable: "unicorn", args: []string{"--version"}, regex: "(?P.*)", }, }, extensions: []string{uni, corn}, enabledExtensions: []string{uni}, enabledCommands: []string{"unicorn"}, version: universion, } lang := bootStrapLanguageTest(args) assert.True(t, lang.Enabled()) assert.Equal(t, universion, lang.Full, "unicorn is available and uni files are found") assert.Equal(t, "unicorn", lang.Executable, "unicorn was used") } func TestLanguageEnabledMismatch(t *testing.T) { expectedVersion := "1.2.009" args := &languageArgs{ commands: []*cmd{ { executable: "unicorn", args: []string{"--version"}, regex: "(?P.*)", }, }, extensions: []string{uni, corn}, enabledExtensions: []string{uni}, enabledCommands: []string{"unicorn"}, version: universion, matchesVersionFile: func() (string, bool) { return expectedVersion, false }, } lang := bootStrapLanguageTest(args) assert.True(t, lang.Enabled()) assert.Equal(t, expectedVersion, lang.Expected, "the expected unicorn version is 1.2.009") assert.True(t, lang.Mismatch, "we require a different version of unicorn") } func TestLanguageDisabledInHome(t *testing.T) { args := &languageArgs{ commands: []*cmd{ { executable: "unicorn", args: []string{"--version"}, regex: "(?P.*)", }, }, extensions: []string{uni, corn}, enabledExtensions: []string{uni}, enabledCommands: []string{"unicorn"}, version: universion, inHome: true, } lang := bootStrapLanguageTest(args) assert.False(t, lang.Enabled()) } func TestLanguageEnabledSecondExtensionFound(t *testing.T) { args := &languageArgs{ commands: []*cmd{ { executable: "unicorn", args: []string{"--version"}, regex: "(?P.*)", }, }, extensions: []string{uni, corn}, enabledExtensions: []string{corn}, enabledCommands: []string{"unicorn"}, version: universion, } lang := bootStrapLanguageTest(args) assert.True(t, lang.Enabled()) assert.Equal(t, universion, lang.Full, "unicorn is available and corn files are found") assert.Equal(t, "unicorn", lang.Executable, "unicorn was used") } func TestLanguageEnabledSecondCommand(t *testing.T) { args := &languageArgs{ commands: []*cmd{ { executable: "uni", args: []string{"--version"}, regex: "(?P.*)", }, { executable: "corn", args: []string{"--version"}, regex: "(?P.*)", }, }, extensions: []string{uni, corn}, enabledExtensions: []string{corn}, enabledCommands: []string{"corn"}, version: universion, } lang := bootStrapLanguageTest(args) assert.True(t, lang.Enabled()) assert.Equal(t, universion, lang.Full, "unicorn is available and corn files are found") assert.Equal(t, "corn", lang.Executable, "corn was used") } func TestLanguageEnabledAllExtensionsFound(t *testing.T) { args := &languageArgs{ commands: []*cmd{ { executable: "unicorn", args: []string{"--version"}, regex: "(?P.*)", }, }, extensions: []string{uni, corn}, enabledExtensions: []string{uni, corn}, enabledCommands: []string{"unicorn"}, version: universion, } lang := bootStrapLanguageTest(args) assert.True(t, lang.Enabled()) assert.Equal(t, universion, lang.Full, "unicorn is available and uni and corn files are found") assert.Equal(t, "unicorn", lang.Executable, "unicorn was used") } func TestLanguageEnabledNoVersion(t *testing.T) { props := options.Map{ options.FetchVersion: false, } args := &languageArgs{ commands: []*cmd{ { executable: "unicorn", args: []string{"--version"}, regex: "(?P.*)", }, }, extensions: []string{uni, corn}, enabledExtensions: []string{uni, corn}, enabledCommands: []string{"unicorn"}, version: universion, options: props, } lang := bootStrapLanguageTest(args) assert.True(t, lang.Enabled()) assert.Equal(t, "", lang.Full, "unicorn is available and uni and corn files are found") assert.Equal(t, "", lang.Executable, "no version was found") } func TestLanguageEnabledMissingCommand(t *testing.T) { props := options.Map{ options.FetchVersion: false, } args := &languageArgs{ commands: []*cmd{}, extensions: []string{uni, corn}, enabledExtensions: []string{uni, corn}, enabledCommands: []string{"unicorn"}, version: universion, options: props, } lang := bootStrapLanguageTest(args) assert.True(t, lang.Enabled()) assert.Equal(t, "", lang.Full, "unicorn is unavailable and uni and corn files are found") assert.Equal(t, "", lang.Executable, "no executable was found") } func TestLanguageEnabledNoVersionData(t *testing.T) { props := options.Map{ options.FetchVersion: true, } args := &languageArgs{ commands: []*cmd{ { executable: "uni", args: []string{"--version"}, regex: `(?:Python (?P((?P[0-9]+).(?P[0-9]+).(?P[0-9]+))))`, }, }, extensions: []string{uni, corn}, enabledExtensions: []string{uni, corn}, enabledCommands: []string{"uni"}, version: "", options: props, } lang := bootStrapLanguageTest(args) assert.True(t, lang.Enabled()) assert.Equal(t, "", lang.Full) assert.Equal(t, "", lang.Executable, "no version was found") } func TestLanguageEnabledMissingCommandCustomText(t *testing.T) { expected := "missing" props := options.Map{ MissingCommandText: expected, } args := &languageArgs{ commands: []*cmd{}, extensions: []string{uni, corn}, enabledExtensions: []string{uni, corn}, enabledCommands: []string{"unicorn"}, version: universion, options: props, } lang := bootStrapLanguageTest(args) assert.True(t, lang.Enabled()) assert.Equal(t, expected, lang.Error, "unicorn is available and uni and corn files are found") } func TestLanguageEnabledMissingCommandCustomTextHideError(t *testing.T) { props := options.Map{MissingCommandText: "missing"} args := &languageArgs{ commands: []*cmd{}, extensions: []string{uni, corn}, enabledExtensions: []string{uni, corn}, enabledCommands: []string{"unicorn"}, version: universion, options: props, } lang := bootStrapLanguageTest(args) assert.True(t, lang.Enabled()) assert.Equal(t, "", lang.Full) } func TestLanguageEnabledCommandExitCode(t *testing.T) { expected := 200 args := &languageArgs{ commands: []*cmd{ { executable: "uni", args: []string{"--version"}, regex: `(?P((?P[0-9]+).(?P[0-9]+).(?P[0-9]+)))`, }, }, extensions: []string{uni, corn}, enabledExtensions: []string{uni, corn}, enabledCommands: []string{"uni"}, version: universion, expectedError: &runtime.CommandError{ExitCode: expected}, } lang := bootStrapLanguageTest(args) assert.True(t, lang.Enabled()) assert.Equal(t, "err executing uni with [--version]", lang.Error) assert.Equal(t, expected, lang.exitCode) } func TestLanguageHyperlinkEnabled(t *testing.T) { args := &languageArgs{ commands: []*cmd{ { executable: "uni", args: []string{"--version"}, regex: `(?P((?P[0-9]+).(?P[0-9]+).(?P[0-9]+)))`, }, { executable: "corn", args: []string{"--version"}, regex: `(?P((?P[0-9]+).(?P[0-9]+).(?P[0-9]+)))`, }, }, versionURLTemplate: "https://unicor.org/doc/{{ .Full }}", extensions: []string{uni, corn}, enabledExtensions: []string{corn}, enabledCommands: []string{"corn"}, version: universion, options: options.Map{}, } lang := bootStrapLanguageTest(args) assert.True(t, lang.Enabled()) assert.Equal(t, "https://unicor.org/doc/1.3.307", lang.URL) } func TestLanguageHyperlinkEnabledWrongRegex(t *testing.T) { args := &languageArgs{ commands: []*cmd{ { executable: "uni", args: []string{"--version"}, regex: `wrong`, }, { executable: "corn", args: []string{"--version"}, regex: `wrong`, }, }, versionURLTemplate: "https://unicor.org/doc/{{ .Full }}", extensions: []string{uni, corn}, enabledExtensions: []string{corn}, enabledCommands: []string{"corn"}, version: universion, options: options.Map{}, } lang := bootStrapLanguageTest(args) assert.True(t, lang.Enabled()) assert.Equal(t, "err parsing info from corn with 1.3.307", lang.Error) } func TestLanguageEnabledInHome(t *testing.T) { cases := []struct { Case string HomeEnabled bool ExpectedEnabled bool }{ {Case: "Always enabled", HomeEnabled: true, ExpectedEnabled: true}, {Case: "Context disabled", HomeEnabled: false, ExpectedEnabled: false}, } for _, tc := range cases { props := options.Map{ HomeEnabled: tc.HomeEnabled, } args := &languageArgs{ commands: []*cmd{ { executable: "uni", args: []string{"--version"}, regex: `(?P((?P[0-9]+).(?P[0-9]+).(?P[0-9]+)))`, }, }, extensions: []string{uni, corn}, enabledExtensions: []string{corn}, enabledCommands: []string{"corn"}, version: universion, options: props, inHome: true, } lang := bootStrapLanguageTest(args) assert.Equal(t, tc.ExpectedEnabled, lang.Enabled(), tc.Case) } } func TestLanguageInnerHyperlink(t *testing.T) { args := &languageArgs{ commands: []*cmd{ { executable: "uni", args: []string{"--version"}, regex: `(?P((?P[0-9]+).(?P[0-9]+).(?P[0-9]+)))`, versionURLTemplate: "https://uni.org/release/{{ .Full }}", }, { executable: "corn", args: []string{"--version"}, regex: `(?P((?P[0-9]+).(?P[0-9]+).(?P[0-9]+)))`, versionURLTemplate: "https://unicor.org/doc/{{ .Full }}", }, }, versionURLTemplate: "This gets replaced with inner template", extensions: []string{uni, corn}, enabledExtensions: []string{corn}, enabledCommands: []string{"corn"}, version: universion, options: options.Map{}, } lang := bootStrapLanguageTest(args) assert.True(t, lang.Enabled()) assert.Equal(t, "https://unicor.org/doc/1.3.307", lang.URL) } func TestLanguageHyperlinkTemplatePropertyTakesPriority(t *testing.T) { args := &languageArgs{ commands: []*cmd{ { executable: "uni", args: []string{"--version"}, regex: `(?P((?P[0-9]+).(?P[0-9]+).(?P[0-9]+)))`, versionURLTemplate: "https://uni.org/release/{{ .Full }}", }, }, extensions: []string{uni}, enabledExtensions: []string{uni}, enabledCommands: []string{"uni"}, version: universion, options: options.Map{ options.VersionURLTemplate: "https://custom/url/template/{{ .Major }}.{{ .Minor }}", }, } lang := bootStrapLanguageTest(args) assert.True(t, lang.Enabled()) assert.Equal(t, "https://custom/url/template/1.3", lang.URL) } func TestLanguageTooling(t *testing.T) { cases := []struct { Case string ExpectedFirst string ExpectedVersion string ToolVersion string DefaultVersion string Tooling []string DefaultTooling []string EnabledTools []string }{ { Case: "Custom tooling overrides default", Tooling: []string{"mytool"}, DefaultTooling: []string{"unicorn"}, EnabledTools: []string{"mytool", "unicorn"}, ExpectedFirst: "mytool", ExpectedVersion: "2.0.0", ToolVersion: "2.0.0", DefaultVersion: "1.0.0", }, { Case: "Default tooling used when no override", Tooling: nil, DefaultTooling: []string{"unicorn"}, EnabledTools: []string{"mytool", "unicorn"}, ExpectedFirst: "unicorn", ExpectedVersion: "1.0.0", ToolVersion: "2.0.0", DefaultVersion: "1.0.0", }, { Case: "Tool not available falls back to next", Tooling: []string{"mytool", "unicorn"}, DefaultTooling: []string{"unicorn"}, EnabledTools: []string{"unicorn"}, ExpectedFirst: "mytool", ExpectedVersion: "1.0.0", ToolVersion: "", DefaultVersion: "1.0.0", }, } for _, tc := range cases { env := new(mock.Environment) env.On("Pwd").Return("/usr/home/project") env.On("Home").Return("/usr/home") env.On("HasFiles", uni).Return(true) hasUnicorn := slices.Contains(tc.EnabledTools, "unicorn") env.On("HasCommand", "unicorn").Return(hasUnicorn) if hasUnicorn { env.On("RunCommand", "unicorn", []string{"--version"}).Return(tc.DefaultVersion, nil) } hasToolCommand := slices.Contains(tc.EnabledTools, "mytool") env.On("HasCommand", "mytool").Return(hasToolCommand) if hasToolCommand { env.On("RunCommand", "mytool", []string{"--version"}).Return(tc.ToolVersion, nil) } props := options.Map{ options.FetchVersion: true, } if tc.Tooling != nil { props[Tooling] = tc.Tooling } l := &Language{ extensions: []string{uni}, defaultTooling: tc.DefaultTooling, tooling: map[string]*cmd{ "unicorn": { executable: "unicorn", args: []string{"--version"}, regex: "(?P.*)", }, "mytool": { executable: "mytool", args: []string{"--version"}, regex: "(?P.*)", }, }, } l.Init(props, env) assert.True(t, l.Enabled(), tc.Case) assert.Equal(t, tc.ExpectedFirst, l.commands[0].executable, tc.Case) assert.Equal(t, tc.ExpectedVersion, l.Full, tc.Case) } } type mockedLanguageParams struct { cmd string versionParam string versionOutput string extension string } func getMockedLanguageEnv(params *mockedLanguageParams) (*mock.Environment, options.Map) { env := new(mock.Environment) env.On("HasCommand", params.cmd).Return(true) env.On("RunCommand", params.cmd, []string{params.versionParam}).Return(params.versionOutput, nil) env.On("HasFiles", params.extension).Return(true) env.On("Pwd").Return("/usr/home/project") env.On("Home").Return("/usr/home") props := options.Map{ options.FetchVersion: true, } return env, props } func TestNodePackageVersion(t *testing.T) { cases := []struct { Case string PackageJSON string Version string ShouldFail bool NoFiles bool }{ {Case: "14.1.5", Version: "14.1.5", PackageJSON: "{ \"name\": \"nx\",\"version\": \"14.1.5\"}"}, {Case: "14.0.0", Version: "14.0.0", PackageJSON: "{ \"name\": \"nx\",\"version\": \"14.0.0\"}"}, {Case: "no files", NoFiles: true, ShouldFail: true}, {Case: "bad data", ShouldFail: true, PackageJSON: "bad data"}, } for _, tc := range cases { var env = new(mock.Environment) env.On("Pwd").Return("posh") path := filepath.Join("posh", "node_modules", "nx") env.On("HasFilesInDir", path, "package.json").Return(!tc.NoFiles) env.On("FileContent", filepath.Join(path, "package.json")).Return(tc.PackageJSON) a := &Language{} a.Init(options.Map{}, env) got, err := a.nodePackageVersion("nx") if tc.ShouldFail { assert.Error(t, err, tc.Case) return } assert.Nil(t, err, tc.Case) assert.Equal(t, tc.Version, got, tc.Case) } } ================================================ FILE: src/segments/lastfm.go ================================================ package segments import ( "encoding/json" "errors" "fmt" "github.com/jandedobbeleer/oh-my-posh/src/log" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" ) type LastFM struct { Base Artist string Track string Full string Icon string Status string } const ( // LastFM username Username options.Option = "username" ) type lmfDate struct { UnixString string `json:"uts"` } type lfmTrackInfo struct { IsPlaying *string `json:"nowplaying,omitempty"` } type Artist struct { Name string `json:"#text"` } type lfmTrack struct { Artist `json:"artist"` Name string `json:"name"` Info *lfmTrackInfo `json:"@attr"` Date lmfDate `json:"date"` } type tracks struct { Tracks []*lfmTrack `json:"track"` } type lfmDataResponse struct { TracksInfo tracks `json:"recenttracks"` } func (d *LastFM) Enabled() bool { err := d.setStatus() if err != nil { log.Error(err) return false } return true } func (d *LastFM) Template() string { return " {{ .Icon }}{{ if ne .Status \"stopped\" }}{{ .Full }}{{ end }} " } func (d *LastFM) getResult() (*lfmDataResponse, error) { response := new(lfmDataResponse) apikey := d.options.Template(APIKey, ".", d) username := d.options.Template(Username, ".", d) httpTimeout := d.options.Int(options.HTTPTimeout, options.DefaultHTTPTimeout) url := fmt.Sprintf("https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&api_key=%s&user=%s&format=json&limit=1", apikey, username) body, err := d.env.HTTPRequest(url, nil, httpTimeout) if err != nil { return new(lfmDataResponse), err } err = json.Unmarshal(body, &response) if err != nil { return new(lfmDataResponse), err } return response, nil } func (d *LastFM) setStatus() error { q, err := d.getResult() if err != nil { return err } if len(q.TracksInfo.Tracks) == 0 { return errors.New("no data found") } track := q.TracksInfo.Tracks[0] d.Artist = track.Artist.Name d.Track = track.Name d.Full = fmt.Sprintf("%s - %s", d.Artist, d.Track) isPlaying := track.Info != nil && track.Info.IsPlaying != nil && *track.Info.IsPlaying == "true" if isPlaying { d.Icon = d.options.String(PlayingIcon, "\uE602 ") d.Status = "playing" } else { d.Icon = d.options.String(StoppedIcon, "\uF04D ") d.Status = "stopped" } return nil } ================================================ FILE: src/segments/lastfm_test.go ================================================ package segments import ( "errors" "testing" "github.com/jandedobbeleer/oh-my-posh/src/runtime/mock" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" "github.com/stretchr/testify/assert" ) const ( LFMAPIURL = "https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&api_key=key&user=KibbeWater&format=json&limit=1" ) func TestLFMSegmentSingle(t *testing.T) { cases := []struct { Error error Case string APIJSONResponse string ExpectedString string Template string ExpectedEnabled bool }{ { Case: "All Defaults", APIJSONResponse: `{"recenttracks":{"track":[{"artist":{"#text":"C.Gambino"},"name":"Automatic","@attr":{"nowplaying":"true"}}]}}`, ExpectedString: "\uE602 C.Gambino - Automatic", ExpectedEnabled: true, }, { Case: "Custom Template", APIJSONResponse: `{"recenttracks":{"track":[{"artist":{"#text":"C.Gambino"},"name":"Automatic","@attr":{"nowplaying":"true"}}]}}`, ExpectedString: "\uE602 C.Gambino - Automatic", ExpectedEnabled: true, Template: "{{ .Icon }}{{ if ne .Status \"stopped\" }}{{ .Full }}{{ end }}", }, { Case: "Song Stopped", APIJSONResponse: `{"recenttracks":{"track":[{"artist":{"#text":"C.Gambino"},"name":"Automatic","date":{"uts":"1699350223"}}]}}`, ExpectedString: "\uF04D", ExpectedEnabled: true, Template: "{{ .Icon }}", }, { Case: "Error in retrieving data", APIJSONResponse: "nonsense", Error: errors.New("Something went wrong"), ExpectedEnabled: false, }, } for _, tc := range cases { env := &mock.Environment{} props := options.Map{ APIKey: "key", Username: "KibbeWater", options.HTTPTimeout: 20000, } env.On("HTTPRequest", LFMAPIURL).Return([]byte(tc.APIJSONResponse), tc.Error) lfm := &LastFM{} lfm.Init(props, env) enabled := lfm.Enabled() assert.Equal(t, tc.ExpectedEnabled, enabled, tc.Case) if !enabled { continue } if tc.Template == "" { tc.Template = lfm.Template() } assert.Equal(t, tc.ExpectedString, renderTemplate(env, tc.Template, lfm), tc.Case) } } ================================================ FILE: src/segments/lua.go ================================================ package segments type Lua struct { Language } func (l *Lua) Template() string { return languageTemplate } func (l *Lua) Enabled() bool { l.extensions = []string{"*.lua", "*.rockspec"} l.folders = []string{"lua"} l.tooling = map[string]*cmd{ "lua": { executable: "lua", args: []string{"-v"}, regex: `Lua (?P((?P[0-9]+).(?P[0-9]+)(.(?P[0-9]+))?))`, versionURLTemplate: "https://www.lua.org/manual/{{ .Major }}.{{ .Minor }}/readme.html#changes", }, "luajit": { executable: "luajit", args: []string{"-v"}, regex: `LuaJIT (?P((?P[0-9]+).(?P[0-9]+)(.(?P[0-9]+))?))`, versionURLTemplate: "https://github.com/LuaJIT/LuaJIT/tree/v{{ .Major}}.{{ .Minor}}", }, } l.defaultTooling = []string{"lua", "luajit"} return l.Language.Enabled() } ================================================ FILE: src/segments/lua_test.go ================================================ package segments import ( "fmt" "testing" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/template" "github.com/stretchr/testify/assert" ) func TestLua(t *testing.T) { cases := []struct { Case string ExpectedString string Version string ExpectedURL string Tooling []string HasLua bool HasLuaJit bool }{ { Case: "Lua 5.4.4 - default tooling prefers lua", ExpectedString: "5.4.4", Version: "Lua 5.4.4 Copyright (C) 1994-2022 Lua.org, PUC-Rio", ExpectedURL: "https://www.lua.org/manual/5.4/readme.html#changes", HasLua: true, HasLuaJit: true, }, { Case: "Lua 5.0 - tooling set to luajit but missing so fallback to lua", ExpectedString: "5.0", Version: "Lua 5.0 Copyright (C) 1994-2003 Tecgraf, PUC-Rio", ExpectedURL: "https://www.lua.org/manual/5.0/readme.html#changes", HasLua: true, Tooling: []string{"luajit", "lua"}, }, { Case: "LuaJIT 2.0.5 - tooling set to luajit first", ExpectedString: "2.0.5", Version: "LuaJIT 2.0.5 -- Copyright (C) 2005-2017 Mike Pall. http://luajit.org/", HasLuaJit: true, HasLua: true, ExpectedURL: "https://github.com/LuaJIT/LuaJIT/tree/v2.0", Tooling: []string{"luajit"}, }, { Case: "LuaJIT 2.1.0-beta3 - tooling set to lua first but missing so try luajit", ExpectedString: "2.1.0", Version: "LuaJIT 2.1.0-beta3 -- Copyright (C) 2005-2017 Mike Pall. http://luajit.org/", HasLuaJit: true, ExpectedURL: "https://github.com/LuaJIT/LuaJIT/tree/v2.1", Tooling: []string{"lua", "luajit"}, }, } for _, tc := range cases { params := &mockedLanguageParams{ cmd: "lua", versionParam: "-v", versionOutput: tc.Version, extension: "*.lua", } env, props := getMockedLanguageEnv(params) if !tc.HasLua { env.Unset("HasCommand") env.On("HasCommand", "lua").Return(false) } env.On("HasCommand", "luajit").Return(tc.HasLuaJit) env.On("RunCommand", "luajit", []string{"-v"}).Return(tc.Version, nil) env.On("Shell").Return("bash") // Initialize template system for version URL rendering if template.Cache == nil { template.Cache = &cache.Template{} } template.Init(env, nil, nil) if len(tc.Tooling) > 0 { props[Tooling] = tc.Tooling } l := &Lua{} l.Init(props, env) failMsg := fmt.Sprintf("Failed in case: %s", tc.Case) assert.True(t, l.Enabled(), failMsg) assert.Equal(t, tc.ExpectedString, renderTemplate(env, l.Template(), l), failMsg) assert.Equal(t, tc.ExpectedURL, l.URL, failMsg) } } ================================================ FILE: src/segments/mercurial.go ================================================ package segments import ( "strings" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/runtime/path" ) const ( MERCURIALCOMMAND = "hg" hgLogTemplate = "{rev}|{node}|{branch}|{tags}|{bookmarks}" ) type MercurialStatus struct { ScmStatus } func (s *MercurialStatus) add(code string) { switch code { case "R", "!": s.Deleted++ case "A": s.Added++ case "?": s.Untracked++ case "M": s.Modified++ } } type Mercurial struct { Working *MercurialStatus LocalCommitNumber string ChangeSetID string ChangeSetIDShort string Branch string Scm Bookmarks []string Tags []string IsTip bool } func (hg *Mercurial) Template() string { return "hg {{.Branch}} {{if .LocalCommitNumber}}({{.LocalCommitNumber}}:{{.ChangeSetIDShort}}){{end}}{{range .Bookmarks }} \uf02e {{.}}{{end}}{{range .Tags}} \uf02b {{.}}{{end}}{{if .Working.Changed}} \uf044 {{ .Working.String }}{{ end }}" //nolint: lll } func (hg *Mercurial) Enabled() bool { if !hg.shouldDisplay() { return false } statusFormats := hg.options.KeyValueMap(StatusFormats, map[string]string{}) hg.Working = &MercurialStatus{ScmStatus: ScmStatus{Formats: statusFormats}} displayStatus := hg.options.Bool(FetchStatus, false) if displayStatus { hg.setMercurialStatus() } return true } func (hg *Mercurial) CacheKey() (string, bool) { dir, err := hg.env.HasParentFilePath(".hg", true) if err != nil { return "", false } return dir.Path, true } func (hg *Mercurial) shouldDisplay() bool { if !hg.hasCommand(MERCURIALCOMMAND) { return false } hgdir, err := hg.env.HasParentFilePath(".hg", false) if err != nil { return false } hg.setDir(hgdir.ParentFolder) hg.mainSCMDir = hgdir.Path hg.scmDir = hgdir.Path // convert the worktree file path to a windows one when in a WSL shared folder hg.repoRootDir = strings.TrimSuffix(hg.convertToWindowsPath(hgdir.Path), "/.hg") return true } func (hg *Mercurial) setDir(dir string) { dir = path.ReplaceHomeDirPrefixWithTilde(dir) // align with template PWD if hg.env.GOOS() == runtime.WINDOWS { hg.Dir = strings.TrimSuffix(dir, `\.hg`) return } hg.Dir = strings.TrimSuffix(dir, "/.hg") } func (hg *Mercurial) setMercurialStatus() { hg.Branch = hg.command idString := hg.getHgCommandOutput("log", "-r", ".", "--template", hgLogTemplate) if idString == "" { return } idSplit := strings.SplitN(idString, "|", 6) if len(idSplit) != 5 { return } hg.LocalCommitNumber = idSplit[0] hg.ChangeSetID = idSplit[1] if len(hg.ChangeSetID) >= 12 { hg.ChangeSetIDShort = hg.ChangeSetID[:12] } hg.Branch = idSplit[2] hg.Tags = doSplit(idSplit[3]) hg.Bookmarks = doSplit(idSplit[4]) hg.IsTip = false tipIndex := 0 for i, tag := range hg.Tags { if tag == "tip" { hg.IsTip = true tipIndex = i break } } if hg.IsTip { hg.Tags = RemoveAtIndex(hg.Tags, tipIndex) } statusString := hg.getHgCommandOutput("status") if statusString == "" { return } statusLines := strings.SplitSeq(statusString, "\n") for status := range statusLines { hg.Working.add(status[:1]) } } func doSplit(s string) []string { if s == "" { return []string{} } return strings.Split(s, " ") } func RemoveAtIndex(s []string, index int) []string { ret := make([]string, 0) ret = append(ret, s[:index]...) return append(ret, s[index+1:]...) } func (hg *Mercurial) getHgCommandOutput(command string, args ...string) string { args = append([]string{"-R", hg.repoRootDir, command}, args...) val, err := hg.env.RunCommand(hg.command, args...) if err != nil { return "" } return strings.TrimSpace(val) } ================================================ FILE: src/segments/mercurial_test.go ================================================ package segments import ( "testing" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/runtime/mock" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" "github.com/stretchr/testify/assert" ) func TestMercurialEnabledToolNotFound(t *testing.T) { env := new(mock.Environment) env.On("InWSLSharedDrive").Return(false) env.On("HasCommand", "hg").Return(false) env.On("GOOS").Return("") env.On("IsWsl").Return(false) hg := &Mercurial{} hg.Init(options.Map{}, env) assert.False(t, hg.Enabled()) } func TestMercurialEnabledInWorkingDirectory(t *testing.T) { fileInfo := &runtime.FileInfo{ Path: "/dir/hello", ParentFolder: "/dir", IsDir: true, } env := new(mock.Environment) env.On("InWSLSharedDrive").Return(false) env.On("HasCommand", "hg").Return(true) env.On("GOOS").Return("") env.On("IsWsl").Return(false) env.On("HasParentFilePath", ".hg", false).Return(fileInfo, nil) env.On("PathSeparator").Return("/") env.On("Home").Return(poshHome) env.On("Getenv", poshGitEnv).Return("") hg := &Mercurial{} hg.Init(options.Map{}, env) assert.True(t, hg.Enabled()) assert.Equal(t, fileInfo.Path, hg.mainSCMDir) assert.Equal(t, fileInfo.Path, hg.repoRootDir) } func TestMercurialGetIdInfo(t *testing.T) { cases := []struct { ExpectedWorking *MercurialStatus Case string LogOutput string StatusOutput string ExpectedBranch string ExpectedChangeSetID string ExpectedShortID string ExpectedLocalCommitNumber string ExpectedBookmarks []string ExpectedTags []string ExpectedIsTip bool }{ { Case: "nochanges_tip", LogOutput: "123|b6cb23dcb79fe5c2215f1ae8f1a85326a7fed500|branchname|tip|", StatusOutput: "", ExpectedWorking: &MercurialStatus{ScmStatus{ Modified: 0, Added: 0, Deleted: 0, Moved: 0, Untracked: 0, Conflicted: 0, }}, ExpectedBranch: "branchname", ExpectedChangeSetID: "b6cb23dcb79fe5c2215f1ae8f1a85326a7fed500", ExpectedShortID: "b6cb23dcb79f", ExpectedLocalCommitNumber: "123", ExpectedIsTip: true, ExpectedBookmarks: []string{}, ExpectedTags: []string{}, }, { Case: "nochanges", LogOutput: "123|b6cb23dcb79fe5c2215f1ae8f1a85326a7fed500|branchname||", StatusOutput: "", ExpectedWorking: &MercurialStatus{ScmStatus{ Modified: 0, Added: 0, Deleted: 0, Moved: 0, Untracked: 0, Conflicted: 0, }}, ExpectedBranch: "branchname", ExpectedChangeSetID: "b6cb23dcb79fe5c2215f1ae8f1a85326a7fed500", ExpectedShortID: "b6cb23dcb79f", ExpectedLocalCommitNumber: "123", ExpectedIsTip: false, ExpectedBookmarks: []string{}, ExpectedTags: []string{}, }, { Case: "changed", LogOutput: "3|11a953bf0288663b530dd6d65f3c8e0d5f7fddb5|default|tip mytag mytag2|bm1 bm2", StatusOutput: ` M Modified.File ? Untracked.File R Removed.File ! AlsoRemoved.File A Added.File `, ExpectedWorking: &MercurialStatus{ScmStatus{ Modified: 1, Added: 1, Deleted: 2, Moved: 0, Untracked: 1, Conflicted: 0, }}, ExpectedBranch: "default", ExpectedChangeSetID: "11a953bf0288663b530dd6d65f3c8e0d5f7fddb5", ExpectedShortID: "11a953bf0288", ExpectedLocalCommitNumber: "3", ExpectedIsTip: true, ExpectedBookmarks: []string{"bm1", "bm2"}, ExpectedTags: []string{"mytag", "mytag2"}, }, } for _, tc := range cases { fileInfo := &runtime.FileInfo{ Path: "/dir/hello", ParentFolder: "/dir", IsDir: true, } props := options.Map{ FetchStatus: true, } env := new(mock.Environment) env.On("InWSLSharedDrive").Return(false) env.On("HasCommand", "hg").Return(true) env.On("GOOS").Return("") env.On("IsWsl").Return(false) env.On("HasParentFilePath", ".hg", false).Return(fileInfo, nil) env.On("PathSeparator").Return("/") env.On("Home").Return(poshHome) env.On("Getenv", poshGitEnv).Return("") env.MockHgCommand(fileInfo.Path, tc.LogOutput, "log", "-r", ".", "--template", hgLogTemplate) env.MockHgCommand(fileInfo.Path, tc.StatusOutput, "status") hg := &Mercurial{} hg.Init(props, env) if tc.ExpectedWorking != nil { tc.ExpectedWorking.Formats = map[string]string{} } assert.True(t, hg.Enabled()) assert.Equal(t, fileInfo.Path, hg.mainSCMDir) assert.Equal(t, fileInfo.Path, hg.repoRootDir) assert.Equal(t, tc.ExpectedWorking, hg.Working, tc.Case) assert.Equal(t, tc.ExpectedBranch, hg.Branch, tc.Case) assert.Equal(t, tc.ExpectedChangeSetID, hg.ChangeSetID, tc.Case) assert.Equal(t, tc.ExpectedShortID, hg.ChangeSetIDShort, tc.Case) assert.Equal(t, tc.ExpectedLocalCommitNumber, hg.LocalCommitNumber, tc.Case) assert.Equal(t, tc.ExpectedIsTip, hg.IsTip, tc.Case) assert.Equal(t, tc.ExpectedBookmarks, hg.Bookmarks, tc.Case) assert.Equal(t, tc.ExpectedTags, hg.Tags, tc.Case) } } ================================================ FILE: src/segments/mojo.go ================================================ package segments import ( "slices" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" ) type Mojo struct { Venv string Language } func (m *Mojo) Template() string { return " {{ if .Error }}{{ .Error }}{{ else }}{{ if .Venv }}{{ .Venv }} {{ end }}{{ .Full }}{{ end }} " } func (m *Mojo) Enabled() bool { m.extensions = []string{"*.🔥", "*.mojo", "mojoproject.toml"} m.tooling = map[string]*cmd{ "mojo": { executable: "mojo", args: []string{"--version"}, regex: `(?:mojo (?P((?P[0-9]+).(?P[0-9]+).(?P[0-9]+))))`, }, } m.defaultTooling = []string{"mojo"} m.displayMode = m.options.String(DisplayMode, DisplayModeEnvironment) m.Language.loadContext = m.loadContext m.Language.inContext = m.inContext return m.Language.Enabled() } func (m *Mojo) loadContext() { if !m.options.Bool(FetchVirtualEnv, true) { return } // Magic, the official package manager and virtual env manager, // is built on top of pixi: https://github.com/prefix-dev/pixi venv := m.env.Getenv("PIXI_ENVIRONMENT_NAME") if len(venv) > 0 && m.canUseVenvName(venv) { m.Venv = venv } } func (m *Mojo) inContext() bool { return m.Venv != "" } func (m *Mojo) canUseVenvName(name string) bool { defaultNames := []string{"default"} if m.options.Bool(options.DisplayDefault, true) || !slices.Contains(defaultNames, name) { return true } return false } ================================================ FILE: src/segments/mojo_test.go ================================================ package segments import ( "testing" "github.com/alecthomas/assert" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" ) func TestMojoTemplate(t *testing.T) { cases := []struct { Case string Expected string VirtualEnvName string FetchVirtualEnv bool DisplayDefault bool FetchVersion bool }{ { Case: "Virtual environment is present", Expected: "foo 24.5.0", VirtualEnvName: "foo", FetchVirtualEnv: true, DisplayDefault: true, FetchVersion: true, }, { Case: "No virtual environment present", Expected: "24.5.0", VirtualEnvName: "", FetchVirtualEnv: true, DisplayDefault: true, FetchVersion: true, }, { Case: "Hide the virtual environment, but show the version", Expected: "24.5.0", VirtualEnvName: "foo", FetchVirtualEnv: false, DisplayDefault: true, FetchVersion: true, }, { Case: "Show the virtual environment, but hide the version", Expected: "foo", VirtualEnvName: "foo", FetchVirtualEnv: true, DisplayDefault: true, FetchVersion: false, }, { Case: "Show the default virtual environment", Expected: "default 24.5.0", VirtualEnvName: "default", FetchVirtualEnv: true, DisplayDefault: true, FetchVersion: true, }, { Case: "Hide the default virtual environment", Expected: "24.5.0", VirtualEnvName: "default", FetchVirtualEnv: true, DisplayDefault: false, FetchVersion: true, }, } for _, tc := range cases { params := &mockedLanguageParams{ cmd: "mojo", versionParam: "--version", versionOutput: "mojo 24.5.0 (e8aacb95)", extension: "*.mojo", } env, props := getMockedLanguageEnv(params) env.On("Getenv", "PIXI_ENVIRONMENT_NAME").Return(tc.VirtualEnvName) props[options.DisplayDefault] = tc.DisplayDefault props[options.FetchVersion] = tc.FetchVersion props[FetchVirtualEnv] = tc.FetchVirtualEnv props[DisplayMode] = DisplayModeAlways mojo := &Mojo{} mojo.Init(props, env) assert.True(t, mojo.Enabled()) assert.Equal(t, tc.Expected, renderTemplate(env, mojo.Template(), mojo), tc.Case) } } ================================================ FILE: src/segments/mvn.go ================================================ package segments type Mvn struct { Language } func (m *Mvn) Enabled() bool { m.extensions = []string{"pom.xml"} m.tooling = map[string]*cmd{ "mvn": { executable: "mvn", args: []string{"--version"}, regex: `(?P((?P[0-9]+).(?P[0-9]+).(?P[0-9]+)(?:-(?P[a-z]+-[0-9]+))?))`, }, } m.defaultTooling = []string{"mvn"} m.versionURLTemplate = "https://github.com/apache/maven/releases/tag/maven-{{ .Full }}" mvnw, err := m.env.HasParentFilePath("mvnw", false) if err == nil { m.tooling["mvn"].executable = mvnw.Path } return m.Language.Enabled() } func (m *Mvn) Template() string { return languageTemplate } ================================================ FILE: src/segments/mvn_test.go ================================================ package segments import ( "errors" "fmt" "testing" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/alecthomas/assert" ) func TestMvn(t *testing.T) { cases := []struct { Case string ExpectedString string MvnVersion string MvnwVersion string HasMvnw bool }{ { Case: "Maven version", ExpectedString: "1.0.0", MvnVersion: "Apache Maven 1.0.0", HasMvnw: false, MvnwVersion: ""}, { Case: "Local Maven version from wrapper", ExpectedString: "1.1.0-beta-9", MvnVersion: "Apache Maven 1.0.0", HasMvnw: true, MvnwVersion: "Apache Maven 1.1.0-beta-9"}, } for _, tc := range cases { params := &mockedLanguageParams{ cmd: "mvn", versionParam: "--version", versionOutput: tc.MvnVersion, extension: "pom.xml", } env, props := getMockedLanguageEnv(params) fileInfo := &runtime.FileInfo{ Path: "../mvnw", ParentFolder: "./", IsDir: false, } var err error if !tc.HasMvnw { err = errors.New("no match") } env.On("HasParentFilePath", "mvnw", false).Return(fileInfo, err) env.On("HasCommand", fileInfo.Path).Return(tc.HasMvnw) env.On("RunCommand", fileInfo.Path, []string{"--version"}).Return(tc.MvnwVersion, nil) m := &Mvn{} m.Init(props, env) assert.True(t, m.Enabled(), fmt.Sprintf("Failed in case: %s", tc.Case)) assert.Equal(t, tc.ExpectedString, renderTemplate(env, m.Template(), m), fmt.Sprintf("Failed in case: %s", tc.Case)) } } ================================================ FILE: src/segments/nba.go ================================================ package segments import ( "encoding/json" "errors" "fmt" "time" "github.com/jandedobbeleer/oh-my-posh/src/log" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" ) // segment struct, makes templating easier type Nba struct { Base NBAData } // NBA struct contains parsed API data that care about for the segment type NBAData struct { HomeTeam string AwayTeam string Time string GameDate string StartTimeUTC string GameStatus GameStatus // 1 = scheduled, 2 = in progress, 3 = finished HomeScore int AwayScore int HomeTeamWins int HomeTeamLosses int AwayTeamWins int AwayTeamLosses int } func (nba *NBAData) HasStats() bool { return nba.HomeTeamWins != 0 || nba.HomeTeamLosses != 0 || nba.AwayTeamWins != 0 || nba.AwayTeamLosses != 0 } func (nba *NBAData) Started() bool { return nba.GameStatus == InProgress || nba.GameStatus == Finished } const ( NBASeason options.Option = "season" TeamName options.Option = "team" DaysOffset options.Option = "days_offset" ScheduledTemplate options.Option = "scheduled_template" InProgressTemplate options.Option = "in_progress_template" FinishedTemplate options.Option = "finished_template" NBAScoreURL string = "https://cdn.nba.com/static/json/liveData/scoreboard/todaysScoreboard_00.json" NBAScheduleURL string = "https://stats.nba.com/stats/internationalbroadcasterschedule?LeagueID=00&Season=%s&Date=%s&RegionID=1&EST=Y" UNKNOWN = "unknown" currentNBASeason = "2023" NBADateFormat = "02/01/2006" ) // Custom type for GameStatus type GameStatus int // Constants for GameStatus values const ( Scheduled GameStatus = 1 InProgress GameStatus = 2 Finished GameStatus = 3 NotFound GameStatus = 4 ) // Int() method for GameStatus to get its integer representation // This is a helpful method if people want to come up with their own templates func (gs GameStatus) Int() int { return int(gs) } func (gs GameStatus) Valid() bool { return gs == Scheduled || gs == InProgress || gs == Finished } func (gs GameStatus) String() string { switch gs { case Scheduled: return "Scheduled" case InProgress: return "In Progress" case Finished: return "Finished" case NotFound: return "Not Found" default: return UNKNOWN } } // All of the structs needed to retrieve data from the live score endpoint type ScoreboardResponse struct { Scoreboard Scoreboard `json:"scoreboard"` } type Scoreboard struct { GameDate string `json:"gameDate"` Games []Game `json:"games"` } type Game struct { GameStatusText string `json:"gameStatusText"` GameTimeUTC string `json:"gameTimeUTC"` HomeTeam Team `json:"homeTeam"` AwayTeam Team `json:"awayTeam"` GameStatus int `json:"gameStatus"` } type Team struct { TeamTricode string `json:"teamTricode"` Wins int `json:"wins"` Losses int `json:"losses"` Score int `json:"score"` } // All the structs needed to get data from the schedule endpoint type ScheduleResponse struct { ResultSets []ResultSet `json:"resultSets"` } type ResultSet struct { CompleteGameList []ScheduledGame `json:"CompleteGameList,omitempty"` } type ScheduledGame struct { VtAbbreviation string `json:"vtAbbreviation"` HtAbbreviation string `json:"htAbbreviation"` Date string `json:"date"` Time string `json:"time"` } func (nba *Nba) Template() string { return " \U000F0806 {{ .HomeTeam}}{{ if .HasStats }} ({{.HomeTeamWins}}-{{.HomeTeamLosses}}){{ end }}{{ if .Started }}:{{.HomeScore}}{{ end }} vs {{ .AwayTeam}}{{ if .HasStats }} ({{.AwayTeamWins}}-{{.AwayTeamLosses}}){{ end }}{{ if .Started }}:{{.AwayScore}}{{ end }} | {{ if not .Started }}{{.GameDate}} | {{ end }}{{.Time}} " //nolint:lll } func (nba *Nba) Enabled() bool { data, err := nba.getResult() if err != nil || !data.GameStatus.Valid() { return false } nba.NBAData = *data return true } // parses through a set of games from the score endpoint and looks for props.team in away or home team func (nba *Nba) findGameScoreByTeamTricode(games []Game, teamTricode string) (*Game, error) { for _, game := range games { if game.HomeTeam.TeamTricode == teamTricode || game.AwayTeam.TeamTricode == teamTricode { return &game, nil } } return nil, errors.New("no game score found for team") } // parses through a set of games from the schedule endpoint and looks for props.team in away or home team func (nba *Nba) findGameSchedulebyTeamTricode(games []ScheduledGame, teamTricode string) (*ScheduledGame, error) { for _, game := range games { if game.VtAbbreviation == teamTricode || game.HtAbbreviation == teamTricode { return &game, nil } } return nil, errors.New("no scheduled game found for team") } // parses the time and date from the schedule endpoint into a UTC time func (nba *Nba) parseTimetoUTC(timeEST, date string) string { combinedTime := date + " " + timeEST timeUTC, err := time.Parse("01/02/2006 03:04 PM", combinedTime) if err != nil { return "" } return timeUTC.UTC().Format("2006-01-02T15:04:05Z") } // retrieves data from the score endpoint func (nba *Nba) retrieveScoreData(teamName string, httpTimeout int) (*NBAData, error) { body, err := nba.env.HTTPRequest(NBAScoreURL, nil, httpTimeout) if err != nil { return nil, err } var scoreboardResponse ScoreboardResponse err = json.Unmarshal(body, &scoreboardResponse) if err != nil { return nil, err } gameInfo, err := nba.findGameScoreByTeamTricode(scoreboardResponse.Scoreboard.Games, teamName) if err != nil { return nil, err } return &NBAData{ AwayTeam: gameInfo.AwayTeam.TeamTricode, HomeTeam: gameInfo.HomeTeam.TeamTricode, Time: gameInfo.GameStatusText, GameDate: scoreboardResponse.Scoreboard.GameDate, StartTimeUTC: gameInfo.GameTimeUTC, GameStatus: GameStatus(gameInfo.GameStatus), HomeScore: gameInfo.HomeTeam.Score, AwayScore: gameInfo.AwayTeam.Score, HomeTeamWins: gameInfo.HomeTeam.Wins, HomeTeamLosses: gameInfo.HomeTeam.Losses, AwayTeamWins: gameInfo.AwayTeam.Wins, AwayTeamLosses: gameInfo.AwayTeam.Losses, }, nil } // Retrieves the data from the schedule endpoint func (nba *Nba) retrieveScheduleData(teamName string, httpTimeout int) (*NBAData, error) { // How many days into the future should we look for a game. numDaysToSearch := nba.options.Int(DaysOffset, 8) nbaSeason := nba.options.String(NBASeason, currentNBASeason) // Get the current date in America/New_York nowNYC := time.Now().In(time.FixedZone("America/New_York", -5*60*60)) // Check to see if a game is scheduled while the numDaysToSearch is greater than 0 for numDaysToSearch > 0 { dateStr := nowNYC.Format(NBADateFormat) urlEndpoint := fmt.Sprintf(NBAScheduleURL, nbaSeason, dateStr) body, err := nba.env.HTTPRequest(urlEndpoint, nil, httpTimeout) if err != nil { return nil, err } var scheduleResponse *ScheduleResponse err = json.Unmarshal(body, &scheduleResponse) if err != nil { return nil, err } // Check if we can find a game for the team gameInfo, err := nba.findGameSchedulebyTeamTricode(scheduleResponse.ResultSets[1].CompleteGameList, teamName) if err != nil { // We didn't find a game for the team on this day, so we need to check the next day nowNYC = nowNYC.AddDate(0, 0, 1) numDaysToSearch-- continue } return &NBAData{ AwayTeam: gameInfo.VtAbbreviation, HomeTeam: gameInfo.HtAbbreviation, Time: gameInfo.Time + " ET", GameDate: gameInfo.Date, StartTimeUTC: nba.parseTimetoUTC(gameInfo.Time, gameInfo.Date), GameStatus: Scheduled, HomeScore: 0, AwayScore: 0, HomeTeamWins: 0, HomeTeamLosses: 0, AwayTeamWins: 0, AwayTeamLosses: 0, }, nil } return nil, errors.New("no scheduled game found for team within DaysOffset days") } // First try to get the data from the score endpoint, if that fails, try the schedule endpoint // The score endpoint usually goes live within 12 hours of a game starting func (nba *Nba) getAvailableGameData(teamName string, httpTimeout int) (*NBAData, error) { // Get the info from the score endpoint data, err := nba.retrieveScoreData(teamName, httpTimeout) if err == nil { return data, nil } // If the score endpoint doesn't have anything get data from the schedule endpoint data, err = nba.retrieveScheduleData(teamName, httpTimeout) if err == nil { return data, nil } return nil, err } func (nba *Nba) getResult() (*NBAData, error) { teamName := nba.options.String(TeamName, "") httpTimeout := nba.options.Int(options.HTTPTimeout, options.DefaultHTTPTimeout) log.Debug("fetching available data for " + teamName) data, err := nba.getAvailableGameData(teamName, httpTimeout) if err != nil { log.Error(errors.Join(err, fmt.Errorf("unable to get data for team %s", teamName))) return nil, err } if !data.GameStatus.Valid() { err := fmt.Errorf("%d is not a valid game status", data.GameStatus) log.Error(err) return nil, err } return data, nil } ================================================ FILE: src/segments/nba_test.go ================================================ package segments import ( "fmt" "os" "testing" "time" "github.com/jandedobbeleer/oh-my-posh/src/runtime/mock" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" "github.com/stretchr/testify/assert" ) func getTestData(file string) string { content, _ := os.ReadFile(fmt.Sprintf("../test/%s", file)) return string(content) } // create Test segment for NBA segment func TestNBASegment(t *testing.T) { jsonScheduleData := getTestData("nba/schedule.json") jsonScoreData := getTestData("nba/score.json") cases := []struct { Error error Case string JSONResponse string ExpectedString string TeamName string CacheTimeout int DaysOffset int ExpectedEnabled bool CacheFoundFail bool }{ { Case: "Team (Home Team) Scheduled Game", JSONResponse: jsonScheduleData, TeamName: "LAL", ExpectedString: "󰠆 LAL vs PHX | 10/26/2023 | 10:00 PM ET", ExpectedEnabled: true, DaysOffset: 8, }, { Case: "Team (Away Team) Scheduled Game", JSONResponse: jsonScheduleData, TeamName: "PHX", ExpectedString: "󰠆 LAL vs PHX | 10/26/2023 | 10:00 PM ET", DaysOffset: 4, ExpectedEnabled: true, }, { Case: "Team (Home Team) Live Game", JSONResponse: jsonScoreData, TeamName: "CHA", ExpectedString: "󰠆 CHA (1-0):13 vs BOS (0-1):8 | Q1 8:23", ExpectedEnabled: true, }, { Case: "Team (Away Team) Live Game", JSONResponse: jsonScoreData, TeamName: "BOS", ExpectedString: "󰠆 CHA (1-0):13 vs BOS (0-1):8 | Q1 8:23", ExpectedEnabled: true, }, { Case: "Team not Found", JSONResponse: jsonScheduleData, DaysOffset: 8, TeamName: "INVALID", ExpectedEnabled: false, }, } for _, tc := range cases { env := &mock.Environment{} props := options.Map{ TeamName: tc.TeamName, DaysOffset: tc.DaysOffset, } env.On("HTTPRequest", NBAScoreURL).Return([]byte(tc.JSONResponse), tc.Error) // Add all the daysOffset to the http request responses for i := 0; i < tc.DaysOffset; i++ { currTime := time.Now().In(time.FixedZone("America/New_York", -5*60*60)) // add offset days to currTime so we can query for games in the future currTime = currTime.AddDate(0, 0, i) dateStr := currTime.Format(NBADateFormat) scheduleURLEndpoint := fmt.Sprintf(NBAScheduleURL, currentNBASeason, dateStr) env.On("HTTPRequest", scheduleURLEndpoint).Return([]byte(tc.JSONResponse), tc.Error) } nba := &Nba{} nba.Init(props, env) enabled := nba.Enabled() assert.Equal(t, tc.ExpectedEnabled, enabled, tc.Case) if !enabled { continue } assert.Equal(t, tc.ExpectedString, renderTemplate(env, nba.Template(), nba), tc.Case) } } ================================================ FILE: src/segments/nbgv.go ================================================ package segments import ( "encoding/json" ) type Nbgv struct { Base VersionInfo } type VersionInfo struct { Version string `json:"Version"` AssemblyVersion string `json:"AssemblyVersion"` AssemblyInformationalVersion string `json:"AssemblyInformationalVersion"` NuGetPackageVersion string `json:"NuGetPackageVersion"` ChocolateyPackageVersion string `json:"ChocolateyPackageVersion"` NpmPackageVersion string `json:"NpmPackageVersion"` SimpleVersion string `json:"SimpleVersion"` VersionFileFound bool `json:"VersionFileFound"` } func (n *Nbgv) Template() string { return " {{ .Version }} " } func (n *Nbgv) Enabled() bool { nbgv := "nbgv" if !n.env.HasCommand(nbgv) { return false } response, err := n.env.RunCommand(nbgv, "get-version", "--format=json") if err != nil { return false } n.VersionInfo = VersionInfo{} err = json.Unmarshal([]byte(response), &n.VersionInfo) if err != nil { return false } return n.VersionFileFound } ================================================ FILE: src/segments/nbgv_test.go ================================================ package segments import ( "errors" "testing" "github.com/jandedobbeleer/oh-my-posh/src/runtime/mock" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" "github.com/alecthomas/assert" ) func TestNbgv(t *testing.T) { cases := []struct { Error error Case string ExpectedString string Response string Template string ExpectedEnabled bool HasNbgv bool }{ {Case: "nbgv not installed"}, {Case: "nbgv installed, no version file", HasNbgv: true, Response: "{ \"VersionFileFound\": false }"}, {Case: "nbgv installed with version file", ExpectedEnabled: true, HasNbgv: true, Response: "{ \"VersionFileFound\": true }"}, { Case: "invalid template", ExpectedEnabled: true, ExpectedString: "invalid template text", HasNbgv: true, Response: "{ \"VersionFileFound\": true }", Template: "{{ err }}", }, { Case: "command error", HasNbgv: true, Error: errors.New("oh noes"), }, { Case: "invalid json", HasNbgv: true, Response: "><<<>>>", }, { Case: "Version", ExpectedEnabled: true, ExpectedString: "bump", HasNbgv: true, Response: "{ \"VersionFileFound\": true, \"Version\": \"bump\" }", Template: "{{ .Version }}", }, { Case: "AssemblyVersion", ExpectedEnabled: true, ExpectedString: "bump", HasNbgv: true, Response: "{ \"VersionFileFound\": true, \"AssemblyVersion\": \"bump\" }", Template: "{{ .AssemblyVersion }}", }, } for _, tc := range cases { env := new(mock.Environment) env.On("HasCommand", "nbgv").Return(tc.HasNbgv) env.On("RunCommand", "nbgv", []string{"get-version", "--format=json"}).Return(tc.Response, tc.Error) nbgv := &Nbgv{} nbgv.Init(options.Map{}, env) enabled := nbgv.Enabled() assert.Equal(t, tc.ExpectedEnabled, enabled, tc.Case) if tc.Template == "" { tc.Template = nbgv.Template() } if enabled { assert.Equal(t, tc.ExpectedString, renderTemplate(env, tc.Template, nbgv), tc.Case) } } } ================================================ FILE: src/segments/nightscout.go ================================================ package segments import ( "encoding/json" "errors" "fmt" http2 "net/http" "time" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" ) // segment struct, makes templating easier type Nightscout struct { Base TrendIcon string NightscoutData } const ( // Your complete Nightscout URL and APIKey like this URL options.Option = "url" Headers options.Option = "headers" DoubleUpIcon options.Option = "doubleup_icon" SingleUpIcon options.Option = "singleup_icon" FortyFiveUpIcon options.Option = "fortyfiveup_icon" FlatIcon options.Option = "flat_icon" FortyFiveDownIcon options.Option = "fortyfivedown_icon" SingleDownIcon options.Option = "singledown_icon" DoubleDownIcon options.Option = "doubledown_icon" ) // NightscoutData struct contains the API data type NightscoutData struct { DateString time.Time `json:"dateString"` SysTime time.Time `json:"sysTime"` ID string `json:"_id"` Direction string `json:"direction"` Device string `json:"device"` Type string `json:"type"` Sgv int `json:"sgv"` Date int64 `json:"date"` Trend int `json:"trend"` UtcOffset int `json:"utcOffset"` Mills int64 `json:"mills"` } // UnmarshalJSON handles both integer and floating-point JSON numbers for the date field. // Some Nightscout API providers (e.g. T1Pal) return the date as a float. func (n *NightscoutData) UnmarshalJSON(data []byte) error { type Alias NightscoutData aux := &struct { *Alias Date json.Number `json:"date"` }{ Alias: (*Alias)(n), } if err := json.Unmarshal(data, aux); err != nil { return err } if aux.Date == "" { return nil } if i, err := aux.Date.Int64(); err == nil { n.Date = i return nil } if f, err := aux.Date.Float64(); err == nil { n.Date = int64(f) return nil } return fmt.Errorf("date field must be a valid number, got: %s", aux.Date) } func (ns *Nightscout) Template() string { return " {{ .Sgv }} " } func (ns *Nightscout) Enabled() bool { data, err := ns.getResult() if err != nil { return false } ns.NightscoutData = *data ns.TrendIcon = ns.getTrendIcon() return true } func (ns *Nightscout) getTrendIcon() string { switch ns.Direction { case "DoubleUp": return ns.options.String(DoubleUpIcon, "↑↑") case "SingleUp": return ns.options.String(SingleUpIcon, "↑") case "FortyFiveUp": return ns.options.String(FortyFiveUpIcon, "↗") case "Flat": return ns.options.String(FlatIcon, "→") case "FortyFiveDown": return ns.options.String(FortyFiveDownIcon, "↘") case "SingleDown": return ns.options.String(SingleDownIcon, "↓") case "DoubleDown": return ns.options.String(DoubleDownIcon, "↓↓") default: return "" } } func (ns *Nightscout) getResult() (*NightscoutData, error) { parseSingleElement := func(data []byte) (*NightscoutData, error) { var result []*NightscoutData err := json.Unmarshal(data, &result) if err != nil { return nil, err } if len(result) == 0 { return nil, errors.New("no elements in the array") } return result[0], nil } url := ns.options.Template(URL, "", ns) httpTimeout := ns.options.Int(options.HTTPTimeout, options.DefaultHTTPTimeout) headers := ns.options.KeyValueMap(Headers, map[string]string{}) modifiers := func(request *http2.Request) { for key, value := range headers { request.Header.Add(key, value) } } body, err := ns.env.HTTPRequest(url, nil, httpTimeout, modifiers) if err != nil { return nil, err } var arr []*NightscoutData err = json.Unmarshal(body, &arr) if err != nil { return nil, err } data, err := parseSingleElement(body) if err != nil { return nil, err } return data, nil } ================================================ FILE: src/segments/nightscout_test.go ================================================ package segments import ( "encoding/json" "errors" "testing" "github.com/jandedobbeleer/oh-my-posh/src/runtime/mock" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" "github.com/jandedobbeleer/oh-my-posh/src/template" "github.com/stretchr/testify/assert" ) const ( FAKEAPIURL = "FAKE" ) func TestNSSegment(t *testing.T) { cases := []struct { Error error Case string JSONResponse string ExpectedString string Template string CacheTimeout int ExpectedEnabled bool CacheFoundFail bool }{ { Case: "Flat 150", JSONResponse: ` [{"_id":"619d6fa819696e8ded5b2206","sgv":150,"date":1637707537000,"dateString":"2021-11-23T22:45:37.000Z","trend":4,"direction":"Flat","device":"share2","type":"sgv","utcOffset":0,"sysTime":"2021-11-23T22:45:37.000Z","mills":1637707537000}]`, //nolint:lll Template: "\ue2a1 {{.Sgv}}{{.TrendIcon}}", ExpectedString: "\ue2a1 150→", ExpectedEnabled: true, }, { Case: "DoubleDown 50", JSONResponse: ` [{"_id":"619d6fa819696e8ded5b2206","sgv":50,"date":1637707537000,"dateString":"2021-11-23T22:45:37.000Z","trend":4,"direction":"DoubleDown","device":"share2","type":"sgv","utcOffset":0,"sysTime":"2021-11-23T22:45:37.000Z","mills":1637707537000}]`, //nolint:lll Template: "\ue2a1 {{.Sgv}}{{.TrendIcon}}", ExpectedString: "\ue2a1 50↓↓", ExpectedEnabled: true, }, { Case: "DoubleUp 250", JSONResponse: ` [{"_id":"619d6fa819696e8ded5b2206","sgv":250,"date":1637707537000,"dateString":"2021-11-23T22:45:37.000Z","trend":4,"direction":"DoubleUp","device":"share2","type":"sgv","utcOffset":0,"sysTime":"2021-11-23T22:45:37.000Z","mills":1637707537000}]`, //nolint:lll Template: "\ue2a1 {{.Sgv}}{{.TrendIcon}}", ExpectedString: "\ue2a1 250↑↑", ExpectedEnabled: true, }, { Case: "SingleUp 130", JSONResponse: ` [{"_id":"619d6fa819696e8ded5b2206","sgv":130,"date":1637707537000,"dateString":"2021-11-23T22:45:37.000Z","trend":4,"direction":"SingleUp","device":"share2","type":"sgv","utcOffset":0,"sysTime":"2021-11-23T22:45:37.000Z","mills":1637707537000}]`, //nolint:lll Template: "\ue2a1 {{.Sgv}}{{.TrendIcon}}", ExpectedString: "\ue2a1 130↑", ExpectedEnabled: true, }, { Case: "FortyFiveUp 174", JSONResponse: ` [{"_id":"619d6fa819696e8ded5b2206","sgv":174,"date":1637707537000,"dateString":"2021-11-23T22:45:37.000Z","trend":4,"direction":"FortyFiveUp","device":"share2","type":"sgv","utcOffset":0,"sysTime":"2021-11-23T22:45:37.000Z","mills":1637707537000}]`, //nolint:lll Template: "\ue2a1 {{.Sgv}}{{.TrendIcon}}", ExpectedString: "\ue2a1 174↗", ExpectedEnabled: true, }, { Case: "FortyFiveDown 61", JSONResponse: ` [{"_id":"619d6fa819696e8ded5b2206","sgv":61,"date":1637707537000,"dateString":"2021-11-23T22:45:37.000Z","trend":4,"direction":"FortyFiveDown","device":"share2","type":"sgv","utcOffset":0,"sysTime":"2021-11-23T22:45:37.000Z","mills":1637707537000}]`, //nolint:lll Template: "\ue2a1 {{.Sgv}}{{.TrendIcon}}", ExpectedString: "\ue2a1 61↘", ExpectedEnabled: true, }, { Case: "DoubleDown 50", JSONResponse: ` [{"_id":"619d6fa819696e8ded5b2206","sgv":50,"date":1637707537000,"dateString":"2021-11-23T22:45:37.000Z","trend":4,"direction":"DoubleDown","device":"share2","type":"sgv","utcOffset":0,"sysTime":"2021-11-23T22:45:37.000Z","mills":1637707537000}]`, //nolint:lll Template: "\ue2a1 {{.Sgv}}{{.TrendIcon}}", ExpectedString: "\ue2a1 50↓↓", ExpectedEnabled: true, }, { Case: "Float date value", JSONResponse: ` [{"_id":"619d6fa819696e8ded5b2206","sgv":124,"date":1770512410938.386,"dateString":"2026-02-08T01:00:10.000Z","trend":4,"direction":"Flat","device":"share2","type":"sgv","utcOffset":0,"sysTime":"2026-02-08T01:00:10.000Z","mills":1770512410000}]`, //nolint:lll Template: "\ue2a1 {{.Sgv}}{{.TrendIcon}}", ExpectedString: "\ue2a1 124→", ExpectedEnabled: true, }, { Case: "Error in retrieving data", JSONResponse: "nonsense", Error: errors.New("Something went wrong"), ExpectedEnabled: false, }, { Case: "Empty array", JSONResponse: "[]", ExpectedEnabled: false, }, { Case: "Error parsing response", JSONResponse: ` 4tffgt4e4567`, Template: "\ue2a1 {{.Sgv}}{{.TrendIcon}}", ExpectedString: "\ue2a1 50↓↓", ExpectedEnabled: false, }, { Case: "Faulty template", JSONResponse: ` [{"sgv":50,"direction":"DoubleDown"}]`, Template: "\ue2a1 {{.Sgv}}{{.Burp}}", ExpectedString: template.IncorrectTemplate, ExpectedEnabled: true, }, } for _, tc := range cases { env := &mock.Environment{} props := options.Map{ URL: "FAKE", Headers: map[string]string{"Fake-Header": "xxxxx"}, } env.On("HTTPRequest", FAKEAPIURL).Return([]byte(tc.JSONResponse), tc.Error) ns := &Nightscout{} ns.Init(props, env) enabled := ns.Enabled() assert.Equal(t, tc.ExpectedEnabled, enabled, tc.Case) if !enabled { continue } if tc.Template == "" { tc.Template = ns.Template() } assert.Equal(t, tc.ExpectedString, renderTemplate(env, tc.Template, ns), tc.Case) } } func TestNightscoutDataUnmarshalJSON(t *testing.T) { cases := []struct { Case string JSONInput string ExpectedDate int64 ExpectError bool }{ { Case: "Integer date value", JSONInput: `{"date": 1637707537000}`, ExpectedDate: 1637707537000, }, { Case: "Floating-point date value", JSONInput: `{"date": 1637707537000.5}`, ExpectedDate: 1637707537000, }, { Case: "Floating-point date with larger decimal", JSONInput: `{"date": 1637707537123.789}`, ExpectedDate: 1637707537123, }, { Case: "Invalid date value", JSONInput: `{"date": "not-a-number"}`, ExpectError: true, }, { Case: "Missing date field", JSONInput: `{"sgv": 150}`, }, } for _, tc := range cases { var data NightscoutData err := json.Unmarshal([]byte(tc.JSONInput), &data) if tc.ExpectError { assert.Error(t, err, tc.Case) continue } assert.NoError(t, err, tc.Case) assert.Equal(t, tc.ExpectedDate, data.Date, tc.Case) } } ================================================ FILE: src/segments/nim.go ================================================ package segments type Nim struct { Language } func (n *Nim) Template() string { return languageTemplate } func (n *Nim) Enabled() bool { n.extensions = []string{"*.nim", "*.nims"} n.tooling = map[string]*cmd{ "nim": { executable: "nim", args: []string{"--version"}, regex: `Nim Compiler Version (?P(?P\d+)\.(?P\d+)\.(?P\d+))`, }, } n.defaultTooling = []string{"nim"} return n.Language.Enabled() } ================================================ FILE: src/segments/nim_test.go ================================================ package segments import ( "fmt" "testing" "github.com/stretchr/testify/assert" ) func TestNim(t *testing.T) { cases := []struct { Case string ExpectedString string Version string }{ { Case: "Nim 2.2.0", ExpectedString: "2.2.0", Version: "Nim Compiler Version 2.2.0 [MacOSX: arm64]\nCompiled at 2024-11-30\nCopyright (c) 2006-2024 by Andreas Rumpf", }, { Case: "Nim 1.6.12", ExpectedString: "1.6.12", Version: "Nim Compiler Version 1.6.12 [Linux: amd64]\nCompiled at 2023-06-15\nCopyright (c) 2006-2023 by Andreas Rumpf", }, { Case: "Nim 2.0.0", ExpectedString: "2.0.0", Version: "Nim Compiler Version 2.0.0 [Windows: amd64]\nCompiled at 2023-12-25\nCopyright (c) 2006-2023 by Andreas Rumpf", }, } for _, tc := range cases { params := &mockedLanguageParams{ cmd: "nim", versionParam: "--version", versionOutput: tc.Version, extension: "*.nim", } env, props := getMockedLanguageEnv(params) n := &Nim{} n.Init(props, env) assert.True(t, n.Enabled(), fmt.Sprintf("Failed in case: %s", tc.Case)) assert.Equal(t, tc.ExpectedString, renderTemplate(env, n.Template(), n), fmt.Sprintf("Failed in case: %s", tc.Case)) } } ================================================ FILE: src/segments/nixshell.go ================================================ package segments import ( "path/filepath" "strings" ) const ( NONE = "none" ) type NixShell struct { Base Type string } func (n *NixShell) Template() string { return "via {{ .Type }}-shell" } func (n *NixShell) DetectType() string { shellType := n.env.Getenv("IN_NIX_SHELL") switch shellType { case "pure", "impure": return shellType default: if n.InNewNixShell() { return UNKNOWN } return NONE } } // Hack to detect if we're in a `nix shell` (in contrast to a `nix-shell`). // A better way to do this will be enabled by https://github.com/NixOS/nix/issues/6677 // so we check if the PATH contains a nix store. func (n *NixShell) InNewNixShell() bool { paths := filepath.SplitList(n.env.Getenv("PATH")) for _, p := range paths { if strings.Contains(p, "/nix/store") { return true } } return false } func (n *NixShell) Enabled() bool { n.Type = n.DetectType() return n.Type != NONE } ================================================ FILE: src/segments/nixshell_test.go ================================================ package segments import ( "fmt" "testing" "github.com/alecthomas/assert" "github.com/jandedobbeleer/oh-my-posh/src/runtime/mock" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" ) const ( nixPath = "/nix/store/zznw8fnzss1vaqfg5hmv3y79s3hkqczi-devshell-dir/bin" defaultPath = "/users/xyz/testing" fullNixPath = defaultPath + ":" + nixPath ) func TestNixShellSegment(t *testing.T) { cases := []struct { name string expectedString string shellType string enabled bool }{ { name: "Pure Nix Shell", expectedString: "via pure-shell", shellType: "pure", enabled: true, }, { name: "Impure Nix Shell", expectedString: "via impure-shell", shellType: "impure", enabled: true, }, { name: "Unknown Nix Shell", expectedString: "via unknown-shell", shellType: "unknown", enabled: true, }, { name: "No Nix Shell", expectedString: "", shellType: "", enabled: false, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { env := new(mock.Environment) env.On("Getenv", "IN_NIX_SHELL").Return(tc.shellType) path := defaultPath if tc.shellType != "" { path = fullNixPath } env.On("Getenv", "PATH").Return(path) n := NixShell{} n.Init(options.Map{}, env) assert.Equal(t, tc.enabled, n.Enabled(), fmt.Sprintf("Failed in case: %s", tc.name)) if tc.enabled { assert.Equal(t, tc.expectedString, renderTemplate(env, n.Template(), n), tc.name) } }) } } ================================================ FILE: src/segments/node.go ================================================ package segments import ( "fmt" "strings" "github.com/jandedobbeleer/oh-my-posh/src/regex" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" ) type Node struct { PackageManagerIcon string PackageManagerName string Language } const ( // PnpmIcon illustrates PNPM is used PnpmIcon options.Option = "pnpm_icon" // YarnIcon illustrates Yarn is used YarnIcon options.Option = "yarn_icon" // NPMIcon illustrates NPM is used NPMIcon options.Option = "npm_icon" // BunIcon illustrates Bun is used BunIcon options.Option = "bun_icon" // FetchPackageManager shows if Bun, NPM, PNPM, or Yarn is used FetchPackageManager options.Option = "fetch_package_manager" ) func (n *Node) Template() string { return " {{ if .PackageManagerIcon }}{{ .PackageManagerIcon }} {{ end }}{{ .Full }} " } func (n *Node) Enabled() bool { n.extensions = []string{"*.js", "*.ts", "package.json", ".nvmrc", "pnpm-workspace.yaml", ".pnpmfile.cjs", ".vue"} n.tooling = map[string]*cmd{ "node": { executable: "node", args: []string{"--version"}, regex: `(?:v(?P((?P[0-9]+).(?P[0-9]+).(?P[0-9]+))))`, }, } n.defaultTooling = []string{"node"} n.versionURLTemplate = "https://github.com/nodejs/node/blob/main/doc/changelogs/CHANGELOG_V{{ .Major }}.md#{{ .Full }}" n.Language.matchesVersionFile = n.matchesVersionFile n.Language.loadContext = n.loadContext return n.Language.Enabled() } func (n *Node) loadContext() { if !n.options.Bool(FetchPackageManager, false) { return } packageManagerDefinitions := []struct { fileName string name string iconProperty options.Option defaultIcon string }{ { fileName: "pnpm-lock.yaml", name: "pnpm", iconProperty: PnpmIcon, defaultIcon: "\ue865", }, { fileName: "yarn.lock", name: "yarn", iconProperty: YarnIcon, defaultIcon: "\ue6a7", }, { fileName: "bun.lockb", name: "bun", iconProperty: BunIcon, defaultIcon: "\ue76f", }, { fileName: "bun.lock", name: "bun", iconProperty: BunIcon, defaultIcon: "\ue76f", }, { fileName: "package-lock.json", name: "npm", iconProperty: NPMIcon, defaultIcon: "\uE71E", }, { fileName: "package.json", name: "npm", iconProperty: NPMIcon, defaultIcon: "\uE71E", }, } for _, pm := range packageManagerDefinitions { if n.env.HasFiles(pm.fileName) { n.PackageManagerName = pm.name n.PackageManagerIcon = n.options.String(pm.iconProperty, pm.defaultIcon) break } } } func (n *Node) matchesVersionFile() (string, bool) { fileVersion := n.env.FileContent(".nvmrc") if fileVersion == "" { return "", true } fileVersion = strings.TrimSpace(fileVersion) if strings.HasPrefix(fileVersion, "lts/") { fileVersion = strings.ToLower(fileVersion) codeName := strings.TrimPrefix(fileVersion, "lts/") switch codeName { case "argon": fileVersion = "4.9.1" case "boron": fileVersion = "6.17.1" case "carbon": fileVersion = "8.17.0" case "dubnium": fileVersion = "10.24.1" case "erbium": fileVersion = "12.22.12" case "fermium": fileVersion = "14.21.3" case "gallium": fileVersion = "16.20.2" case "hydrogen": fileVersion = "18.20.8" case "iron": fileVersion = "20.19.6" case "jod": fileVersion = "22.21.1" case "krypton": fileVersion = "24.12.0" } } re := fmt.Sprintf( `(?im)^v?%s(\.?%s)?(\.?%s)?$`, n.Major, n.Minor, n.Patch, ) version := strings.TrimSpace(fileVersion) version = strings.TrimPrefix(version, "v") return version, regex.MatchString(re, fileVersion) } ================================================ FILE: src/segments/node_test.go ================================================ package segments import ( "testing" "github.com/jandedobbeleer/oh-my-posh/src/runtime/mock" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" "github.com/alecthomas/assert" ) func TestNodeMatchesVersionFile(t *testing.T) { nodeVersion := Version{ Full: "22.21.1", Major: "22", Minor: "21", Patch: "1", } cases := []struct { Case string ExpectedVersion string RCVersion string Expected bool }{ {Case: "no file context", Expected: true, RCVersion: ""}, {Case: "version match", Expected: true, ExpectedVersion: "22.21.1", RCVersion: "22.21.1"}, {Case: "version match with newline", Expected: true, ExpectedVersion: "22.21.1", RCVersion: "22.21.1\n"}, {Case: "version mismatch", Expected: false, ExpectedVersion: "3.2.1", RCVersion: "3.2.1"}, {Case: "version match in other format", Expected: true, ExpectedVersion: "22.21.1", RCVersion: "v22.21.1"}, {Case: "version match without patch", Expected: true, ExpectedVersion: "22.21", RCVersion: "22.21"}, {Case: "version match without patch in other format", Expected: true, ExpectedVersion: "22.21", RCVersion: "v22.21"}, {Case: "version match without minor", Expected: true, ExpectedVersion: "22", RCVersion: "22"}, {Case: "version match without minor in other format", Expected: true, ExpectedVersion: "22", RCVersion: "v22"}, {Case: "lts match", Expected: true, ExpectedVersion: "22.21.1", RCVersion: "lts/jod"}, {Case: "lts match upper case", Expected: true, ExpectedVersion: "22.21.1", RCVersion: "lts/Jod"}, {Case: "lts mismatch", Expected: false, ExpectedVersion: "8.17.0", RCVersion: "lts/carbon"}, } for _, tc := range cases { env := new(mock.Environment) env.On("FileContent", ".nvmrc").Return(tc.RCVersion) node := &Node{ Language: Language{ Version: nodeVersion, }, } node.Init(options.Map{}, env) version, match := node.matchesVersionFile() assert.Equal(t, tc.Expected, match, tc.Case) assert.Equal(t, tc.ExpectedVersion, version, tc.Case) } } func TestNodeInContext(t *testing.T) { cases := []struct { Case string ExpectedString string hasPNPM bool hasYarn bool hasNPM bool hasDefault bool hasBun bool PkgMgrEnabled bool }{ {Case: "no package manager file", ExpectedString: "", PkgMgrEnabled: true}, {Case: "pnpm", hasPNPM: true, ExpectedString: "pnpm", PkgMgrEnabled: true}, {Case: "yarn", hasYarn: true, ExpectedString: "yarn", PkgMgrEnabled: true}, {Case: "npm", hasNPM: true, ExpectedString: "npm", PkgMgrEnabled: true}, {Case: "default", hasDefault: true, ExpectedString: "npm", PkgMgrEnabled: true}, {Case: "disabled by pnpm", hasPNPM: true, ExpectedString: "", PkgMgrEnabled: false}, {Case: "disabled by yarn", hasYarn: true, ExpectedString: "", PkgMgrEnabled: false}, {Case: "pnpm and npm", hasPNPM: true, hasNPM: true, ExpectedString: "pnpm", PkgMgrEnabled: true}, {Case: "yarn and npm", hasYarn: true, hasNPM: true, ExpectedString: "yarn", PkgMgrEnabled: true}, {Case: "pnpm, yarn, and npm", hasPNPM: true, hasYarn: true, hasNPM: true, ExpectedString: "pnpm", PkgMgrEnabled: true}, {Case: "bun", hasBun: true, ExpectedString: "bun", PkgMgrEnabled: true}, } for _, tc := range cases { env := new(mock.Environment) env.On("HasFiles", "pnpm-lock.yaml").Return(tc.hasPNPM) env.On("HasFiles", "yarn.lock").Return(tc.hasYarn) env.On("HasFiles", "package-lock.json").Return(tc.hasNPM) env.On("HasFiles", "package.json").Return(tc.hasDefault) env.On("HasFiles", "bun.lockb").Return(tc.hasBun) env.On("HasFiles", "bun.lock").Return(tc.hasBun) props := options.Map{ PnpmIcon: "pnpm", YarnIcon: "yarn", NPMIcon: "npm", BunIcon: "bun", FetchPackageManager: tc.PkgMgrEnabled, } node := &Node{} node.Init(props, env) node.loadContext() assert.Equal(t, tc.ExpectedString, node.PackageManagerIcon, tc.Case) } } ================================================ FILE: src/segments/npm.go ================================================ package segments type Npm struct { Language } func (n *Npm) Enabled() bool { n.extensions = []string{"package.json", "package-lock.json"} n.tooling = map[string]*cmd{ "npm": { executable: "npm", args: []string{"--version"}, regex: `(?P((?P[0-9]+).(?P[0-9]+).(?P[0-9]+)))`, }, } n.defaultTooling = []string{"npm"} n.versionURLTemplate = "https://github.com/npm/cli/releases/tag/v{{ .Full }}" return n.Language.Enabled() } func (n *Npm) Template() string { return " \ue71e {{.Full}} " } ================================================ FILE: src/segments/npm_test.go ================================================ package segments import ( "fmt" "testing" "github.com/alecthomas/assert" ) func TestNpm(t *testing.T) { cases := []struct { Case string ExpectedString string Version string }{ {Case: "1.0.0", ExpectedString: "\ue71e 1.0.0", Version: "1.0.0"}, } for _, tc := range cases { params := &mockedLanguageParams{ cmd: "npm", versionParam: "--version", versionOutput: tc.Version, extension: "package.json", } env, props := getMockedLanguageEnv(params) npm := &Npm{} npm.Init(props, env) assert.True(t, npm.Enabled(), fmt.Sprintf("Failed in case: %s", tc.Case)) assert.Equal(t, tc.ExpectedString, renderTemplate(env, npm.Template(), npm), fmt.Sprintf("Failed in case: %s", tc.Case)) } } ================================================ FILE: src/segments/nx.go ================================================ package segments type Nx struct { Language } func (a *Nx) Template() string { return languageTemplate } func (a *Nx) Enabled() bool { a.extensions = []string{"workspace.json", "nx.json"} a.tooling = map[string]*cmd{ "nx": { regex: `(?:(?P((?P[0-9]+).(?P[0-9]+).(?P[0-9]+))))`, getVersion: a.getVersion, }, } a.defaultTooling = []string{"nx"} a.versionURLTemplate = "https://github.com/nrwl/nx/releases/tag/{{.Full}}" return a.Language.Enabled() } func (a *Nx) getVersion() (string, error) { return a.nodePackageVersion("nx") } ================================================ FILE: src/segments/ocaml.go ================================================ package segments type OCaml struct { Language } func (o *OCaml) Template() string { return languageTemplate } func (o *OCaml) Enabled() bool { o.extensions = []string{"*.ml", "*.mli", "dune", "dune-project", "dune-workspace"} o.tooling = map[string]*cmd{ "ocaml": { executable: "ocaml", args: []string{"-version"}, regex: `The OCaml toplevel, version (?P((?P[0-9]+).(?P[0-9]+).(?P[0-9]+))(-(?P[a-z]+))?)`, }, } o.defaultTooling = []string{"ocaml"} return o.Language.Enabled() } ================================================ FILE: src/segments/ocaml_test.go ================================================ package segments import ( "fmt" "testing" "github.com/stretchr/testify/assert" ) func TestOCaml(t *testing.T) { cases := []struct { Case string ExpectedString string Version string }{ {Case: "OCaml 4.12.0", ExpectedString: "4.12.0", Version: "The OCaml toplevel, version 4.12.0"}, {Case: "OCaml 4.11.0", ExpectedString: "4.11.0", Version: "The OCaml toplevel, version 4.11.0"}, {Case: "OCaml 4.13.0", ExpectedString: "4.13.0", Version: "The OCaml toplevel, version 4.13.0"}, } for _, tc := range cases { params := &mockedLanguageParams{ cmd: "ocaml", versionParam: "-version", versionOutput: tc.Version, extension: "*.ml", } env, props := getMockedLanguageEnv(params) o := &OCaml{} o.Init(props, env) assert.True(t, o.Enabled(), fmt.Sprintf("Failed in case: %s", tc.Case)) assert.Equal(t, tc.ExpectedString, renderTemplate(env, o.Template(), o), fmt.Sprintf("Failed in case: %s", tc.Case)) } } ================================================ FILE: src/segments/options/map.go ================================================ package options import ( "encoding/gob" "fmt" "github.com/jandedobbeleer/oh-my-posh/src/color" "github.com/jandedobbeleer/oh-my-posh/src/generics" "github.com/jandedobbeleer/oh-my-posh/src/log" "github.com/jandedobbeleer/oh-my-posh/src/regex" "github.com/jandedobbeleer/oh-my-posh/src/template" ) func init() { gob.Register([]any{}) gob.Register(map[string]any{}) gob.Register(map[any]any{}) gob.Register([]string{}) gob.Register(map[string]string{}) gob.Register([]int{}) gob.Register([]float64{}) gob.Register([]bool{}) gob.Register(int64(0)) gob.Register(uint64(0)) gob.Register(float32(0)) gob.Register(Map{}) gob.Register((*Option)(nil)) gob.Register(map[Option]any{}) } type Provider interface { Color(option Option, defaultValue color.Ansi) color.Ansi Bool(option Option, defaultValue bool) bool String(option Option, defaultValue string) string Template(option Option, defaultValue string, context any) string Float64(option Option, defaultValue float64) float64 Int(option Option, defaultValue int) int KeyValueMap(option Option, defaultValue map[string]string) map[string]string StringArray(option Option, defaultValue []string) []string Any(option Option, defaultValue any) any } // Option defines one property of a segment for context type Option string // general options used across Segments const ( // Style indicates the style to use Style Option = "style" // FetchVersion decides whether to fetch the version number or not FetchVersion Option = "fetch_version" // AlwaysEnabled decides whether or not to always display the info AlwaysEnabled Option = "always_enabled" // VersionURLTemplate is the template to use when building language segment hyperlink VersionURLTemplate Option = "version_url_template" // DisplayError decides whether to display when an error occurs or not DisplayError Option = "display_error" // DisplayDefault hides or shows the default DisplayDefault Option = "display_default" // AccessToken is the access token to use for an API AccessToken Option = "access_token" // RefreshToken is the refresh token to use for an API RefreshToken Option = "refresh_token" // HTTPTimeout timeout used when executing http request HTTPTimeout Option = "http_timeout" // DefaultHTTPTimeout default timeout used when executing http request DefaultHTTPTimeout = 20 // Files to trigger the segment on Files Option = "files" // Duration of the cache CacheDuration Option = "cache_duration" ) type Map map[Option]any func (m Map) String(option Option, defaultValue string) string { val, found := m[option] if !found { log.Debug(fmt.Sprintf("%s: %s", option, defaultValue)) return defaultValue } value := fmt.Sprint(val) log.Debug(fmt.Sprintf("%s: %s", option, value)) return value } // Template resolves the option value as a template and returns the resolved string. // This allows using template syntax like {{ .Env.MY_API_KEY }} in configuration values. // If template rendering fails, it returns the original string value. func (m Map) Template(option Option, defaultValue string, context any) string { value := m.String(option, defaultValue) if value == "" { return value } resolved, err := template.Render(value, context) if err != nil { log.Debug(fmt.Sprintf("%s: template error, using raw value: %s", option, err)) return value } log.Debug(fmt.Sprintf("%s (template resolved): %s", option, resolved)) return resolved } func (m Map) Color(option Option, defaultValue color.Ansi) color.Ansi { val, found := m[option] if !found { log.Debug(fmt.Sprintf("%s: %s", option, defaultValue)) return defaultValue } colorString := color.Ansi(fmt.Sprint(val)) if color.IsAnsiColorName(colorString) { log.Debug(fmt.Sprintf("%s: %s", option, colorString)) return colorString } values := regex.FindNamedRegexMatch(`(?P#[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}|p:.*)`, colorString.String()) if values != nil && values["color"] != "" { value := color.Ansi(values["color"]) log.Debug(fmt.Sprintf("%s: %s", option, value)) return value } log.Debug(fmt.Sprintf("%s: %s", option, defaultValue)) return defaultValue } func (m Map) Bool(option Option, defaultValue bool) bool { val, found := m[option] if !found { log.Debug(fmt.Sprintf("%s: %t", option, defaultValue)) return defaultValue } boolValue, ok := val.(bool) if !ok { log.Debug(fmt.Sprintf("%s: %t", option, defaultValue)) return defaultValue } log.Debug(fmt.Sprintf("%s: %t", option, boolValue)) return boolValue } func (m Map) Float64(option Option, defaultValue float64) float64 { val, found := m[option] if !found { log.Debug(fmt.Sprintf("%s: %f", option, defaultValue)) return defaultValue } // Direct type conversions for common numeric types switch v := val.(type) { case float64: log.Debug(fmt.Sprintf("%s: %f", option, v)) return v case int: value := float64(v) log.Debug(fmt.Sprintf("%s: %f", option, value)) return value case int64: value := float64(v) log.Debug(fmt.Sprintf("%s: %f", option, value)) return value case uint64: value := float64(v) log.Debug(fmt.Sprintf("%s: %f", option, value)) return value default: log.Debug(fmt.Sprintf("%s: %f", option, defaultValue)) return defaultValue } } func (m Map) Int(option Option, defaultValue int) int { val, found := m[option] if !found { log.Debug(fmt.Sprintf("%s: %d", option, defaultValue)) return defaultValue } // Direct type conversions for common numeric types switch v := val.(type) { case int: log.Debug(fmt.Sprintf("%s: %d", option, v)) return v case int64: value := int(v) log.Debug(fmt.Sprintf("%s: %d", option, value)) return value case uint64: value := int(v) log.Debug(fmt.Sprintf("%s: %d", option, value)) return value case float64: value := int(v) log.Debug(fmt.Sprintf("%s: %d", option, value)) return value default: log.Debug(fmt.Sprintf("%s: %d", option, defaultValue)) return defaultValue } } func (m Map) KeyValueMap(option Option, defaultValue map[string]string) map[string]string { val, found := m[option] if !found { log.Debug(fmt.Sprintf("%s: %v", option, defaultValue)) return defaultValue } keyValues := parseKeyValueArray(val) log.Debug(fmt.Sprintf("%s: %v", option, keyValues)) return keyValues } func (m Map) StringArray(option Option, defaultValue []string) []string { val, found := m[option] if !found { log.Debug(fmt.Sprintf("%s: %v", option, defaultValue)) return defaultValue } keyValues := ParseStringArray(val) log.Debug(fmt.Sprintf("%s: %v", option, keyValues)) return keyValues } func (m Map) Any(option Option, defaultValue any) any { val, found := m[option] if !found { log.Debug(fmt.Sprintf("%s: %v", option, defaultValue)) return defaultValue } log.Debug(fmt.Sprintf("%s: %v", option, val)) return val } func ParseStringArray(param any) []string { return generics.ParseStringSlice(param) } func parseKeyValueArray(param any) map[string]string { switch v := param.(type) { default: return map[string]string{} case map[any]any: keyValueArray := make(map[string]string) for key, value := range v { val := value.(string) keyString := fmt.Sprintf("%v", key) keyValueArray[keyString] = val } return keyValueArray case map[string]any: keyValueArray := make(map[string]string) for key, value := range v { val := value.(string) keyValueArray[key] = val } return keyValueArray case []any: keyValueArray := make(map[string]string) for _, s := range v { l := ParseStringArray(s) if len(l) == 2 { key := l[0] val := l[1] keyValueArray[key] = val } } return keyValueArray case Map: keyValueArray := make(map[string]string) for key, value := range v { val := value.(string) keyString := fmt.Sprintf("%v", key) keyValueArray[keyString] = val } return keyValueArray case map[string]string: return v } } // Generic functions type Value interface { string | int | []string | float64 | bool } func OneOf[T Value](options Provider, defaultValue T, props ...Option) T { for _, prop := range props { // get value on a generic get, then see if we can cast to T? val := options.Any(prop, nil) if val == nil { continue } if v, ok := val.(T); ok { return v } } return defaultValue } ================================================ FILE: src/segments/options/map_test.go ================================================ package options import ( "testing" "github.com/jandedobbeleer/oh-my-posh/src/color" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/runtime/mock" "github.com/jandedobbeleer/oh-my-posh/src/template" "github.com/stretchr/testify/assert" ) const ( expected = "expected" expectedColor = color.Ansi("#768954") Foo Option = "color" ) func TestGetString(t *testing.T) { var options = Map{Foo: expected} value := options.String(Foo, "err") assert.Equal(t, expected, value) } func TestGetStringNoEntry(t *testing.T) { var options = Map{} value := options.String(Foo, expected) assert.Equal(t, expected, value) } func TestGetStringNoTextEntry(t *testing.T) { var options = Map{Foo: true} value := options.String(Foo, expected) assert.Equal(t, "true", value) } func TestGetHexColor(t *testing.T) { expected := expectedColor var options = Map{Foo: expected} value := options.Color(Foo, "#789123") assert.Equal(t, expected, value) } func TestGetColor(t *testing.T) { expected := color.Ansi("yellow") var options = Map{Foo: expected} value := options.Color(Foo, "#789123") assert.Equal(t, expected, value) } func TestDefaultColorWithInvalidColorCode(t *testing.T) { expected := expectedColor var options = Map{Foo: "invalid"} value := options.Color(Foo, expected) assert.Equal(t, expected, value) } func TestDefaultColorWithUnavailableProperty(t *testing.T) { expected := expectedColor var options = Map{} value := options.Color(Foo, expected) assert.Equal(t, expected, value) } func TestGetPaletteColor(t *testing.T) { expected := color.Ansi("p:red") var options = Map{Foo: expected} value := options.Color(Foo, "white") assert.Equal(t, expected, value) } func TestGetBool(t *testing.T) { expected := true var options = Map{Foo: expected} value := options.Bool(Foo, false) assert.True(t, value) } func TestGetBoolPropertyNotInMap(t *testing.T) { var options = Map{} value := options.Bool(Foo, false) assert.False(t, value) } func TestGetBoolInvalidProperty(t *testing.T) { var options = Map{Foo: "borked"} value := options.Bool(Foo, false) assert.False(t, value) } func TestGetFloat64(t *testing.T) { cases := []struct { Input any Case string Expected float64 }{ {Case: "int", Expected: 1337, Input: 1337}, {Case: "float64", Expected: 1337, Input: float64(1337)}, {Case: "uint64", Expected: 1337, Input: uint64(1337)}, {Case: "int64", Expected: 1337, Input: int64(1337)}, {Case: "string", Expected: 9001, Input: "invalid"}, {Case: "bool", Expected: 9001, Input: true}, } for _, tc := range cases { options := Map{Foo: tc.Input} value := options.Float64(Foo, 9001) assert.Equal(t, tc.Expected, value, tc.Case) } } func TestGetFloat64PropertyNotInMap(t *testing.T) { expected := float64(1337) var options = Map{} value := options.Float64(Foo, expected) assert.Equal(t, expected, value) } func TestOneOf(t *testing.T) { cases := []struct { Expected any Map Map Case string DefaultValue string Options []Option }{ { Case: "one element", Expected: "1337", Options: []Option{Foo}, Map: Map{ Foo: "1337", }, DefaultValue: "2000", }, { Case: "two elements", Expected: "1337", Options: []Option{Foo}, Map: Map{ Foo: "1337", "Bar": "9001", }, DefaultValue: "2000", }, { Case: "no match", Expected: "2000", Options: []Option{"Moo"}, Map: Map{ Foo: "1337", "Bar": "9001", }, DefaultValue: "2000", }, { Case: "incorrect type", Expected: "2000", Options: []Option{Foo}, Map: Map{ Foo: 1337, "Bar": "9001", }, DefaultValue: "2000", }, } for _, tc := range cases { value := OneOf(tc.Map, tc.DefaultValue, tc.Options...) assert.Equal(t, tc.Expected, value, tc.Case) } } func TestTemplate(t *testing.T) { // Need to initialize template package for testing env := &mock.Environment{} env.On("Getenv", "MY_API_KEY").Return("secret-key-123") env.On("Getenv", "MY_USER").Return("testuser") env.On("Getenv", "SHLVL").Return("1") env.On("Shell").Return("bash") env.On("Flags").Return(&runtime.Flags{ IsPrimary: true, ShellVersion: "1.0.0", PromptCount: 1, JobCount: 0, PSWD: "/home/test", AbsolutePWD: "/home/test", }) env.On("Root").Return(false) env.On("StatusCodes").Return(0, "0") env.On("IsWsl").Return(false) env.On("Pwd").Return("/home/test") env.On("GOOS").Return(runtime.LINUX) env.On("Platform").Return("ubuntu") env.On("User").Return("testuser") env.On("Host").Return("testhost", nil) // Initialize template package template.Init(env, nil, nil) cases := []struct { Case string Options Map Option Option DefaultValue string Context any Expected string }{ { Case: "plain string no template", Options: Map{"key": "plain-value"}, Option: "key", DefaultValue: "", Context: nil, Expected: "plain-value", }, { Case: "template with env var", Options: Map{"key": "{{ .Env.MY_API_KEY }}"}, Option: "key", DefaultValue: "", Context: nil, Expected: "secret-key-123", }, { Case: "template with multiple env vars", Options: Map{"key": "{{ .Env.MY_USER }}/{{ .Env.MY_API_KEY }}"}, Option: "key", DefaultValue: "", Context: nil, Expected: "testuser/secret-key-123", }, { Case: "empty value returns default", Options: Map{}, Option: "key", DefaultValue: "default-value", Context: nil, Expected: "default-value", }, { Case: "invalid template returns raw value", Options: Map{"key": "{{ .Invalid }}"}, Option: "key", DefaultValue: "", Context: nil, Expected: "{{ .Invalid }}", }, } for _, tc := range cases { value := tc.Options.Template(tc.Option, tc.DefaultValue, tc.Context) assert.Equal(t, tc.Expected, value, tc.Case) } } ================================================ FILE: src/segments/os.go ================================================ package segments import ( "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" ) type Os struct { Base Icon string } const ( // MacOS the string/icon to use for MacOS MacOS options.Option = "macos" // Linux the string/icon to use for linux Linux options.Option = "linux" // Windows the string/icon to use for windows Windows options.Option = "windows" // Android the string/icon to use for android Android options.Option = "android" // DisplayDistroName display the distro name or not DisplayDistroName options.Option = "display_distro_name" ) func (oi *Os) Template() string { return " {{ if .WSL }}WSL at {{ end }}{{.Icon}} " } func (oi *Os) Enabled() bool { goos := oi.env.GOOS() switch goos { case runtime.WINDOWS: oi.Icon = oi.options.String(Windows, "\uE62A") case runtime.DARWIN: oi.Icon = oi.options.String(MacOS, "\uF179") case runtime.LINUX, runtime.FREEBSD: pf := oi.env.Platform() displayDistroName := oi.options.Bool(DisplayDistroName, false) if displayDistroName { oi.Icon = oi.options.String(options.Option(pf), pf) break } oi.Icon = oi.getDistroIcon(pf) case runtime.ANDROID: oi.Icon = oi.options.String(Android, "\ue70e") default: oi.Icon = goos } return true } func (oi *Os) getDistroIcon(distro string) string { iconMap := map[string]string{ "alma": "\uf31d", "almalinux": "\uf31d", "almalinux9": "\uf31d", "alpine": "\uf300", "android": "\ue70e", "aosc": "\uf301", "arch": "\uf303", "centos": "\uf304", "coreos": "\uf305", "debian": "\uf306", "deepin": "\uf321", "devuan": "\uf307", "elementary": "\uf309", "endeavouros": "\uf322", "fedora": "\uf30a", "freebsd": "\U000f08e0", "gentoo": "\uf30d", "kali": "\uf327", "mageia": "\uf310", "manjaro": "\uf312", "mint": "\U000f08ed", "neon": "\uf331", "nixos": "\uf313", "opensuse": "\uf314", "opensuse-tumbleweed": "\uf314", "raspbian": "\uf315", "redhat": "\uf316", "rocky": "\uf32b", "sabayon": "\uf317", "slackware": "\uf319", "ubuntu": "\uf31b", "void": "\uf32e", "zorin": "\uf32f", } if icon, ok := iconMap[distro]; ok { return oi.options.String(options.Option(distro), icon) } icon := oi.options.String(options.Option(distro), "") if len(icon) > 0 { return icon } return oi.options.String(Linux, "\uF17C") } ================================================ FILE: src/segments/os_test.go ================================================ package segments import ( "testing" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/runtime/mock" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" "github.com/jandedobbeleer/oh-my-posh/src/template" "github.com/stretchr/testify/assert" ) func TestOSInfo(t *testing.T) { cases := []struct { Case string ExpectedString string GOOS string Platform string Icon string IsWSL bool DisplayDistroName bool }{ { Case: "WSL debian - icon", ExpectedString: "WSL at \uf306", GOOS: "linux", IsWSL: true, Platform: "debian", }, { Case: "WSL debian - name", ExpectedString: "WSL at debian", GOOS: "linux", IsWSL: true, Platform: "debian", DisplayDistroName: true, }, { Case: "plain linux - icon", ExpectedString: "\uf306", GOOS: "linux", Platform: "debian", }, { Case: "plain linux - name", ExpectedString: "debian", GOOS: "linux", Platform: "debian", DisplayDistroName: true, }, { Case: "windows", ExpectedString: "windows", GOOS: "windows", }, { Case: "darwin", ExpectedString: "darwin", GOOS: "darwin", }, { Case: "unknown", ExpectedString: "unknown", GOOS: "unknown", }, { Case: "crazy distro, specific icon", ExpectedString: "crazy distro", GOOS: "linux", Platform: "crazy", Icon: "crazy distro", }, { Case: "crazy distro, not mapped", ExpectedString: "\uf17c", GOOS: "linux", Platform: "crazy", }, { Case: "show distro name, mapped", ExpectedString: "<3", DisplayDistroName: true, GOOS: "linux", Icon: "<3", Platform: "love", }, } for _, tc := range cases { env := new(mock.Environment) env.On("GOOS").Return(tc.GOOS) env.On("Platform").Return(tc.Platform) props := options.Map{ DisplayDistroName: tc.DisplayDistroName, Windows: "windows", MacOS: "darwin", } if len(tc.Icon) != 0 { props[options.Option(tc.Platform)] = tc.Icon } osInfo := &Os{} osInfo.Init(props, env) template.Cache = &cache.Template{ SimpleTemplate: cache.SimpleTemplate{ WSL: tc.IsWSL, }, } _ = osInfo.Enabled() assert.Equal(t, tc.ExpectedString, renderTemplate(env, osInfo.Template(), osInfo), tc.Case) } } ================================================ FILE: src/segments/owm.go ================================================ package segments import ( "encoding/json" "errors" "fmt" "math" "net/url" "github.com/jandedobbeleer/oh-my-posh/src/log" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" ) type Owm struct { Base Weather string URL string units string UnitIcon string Temperature int } const ( // APIKey openweathermap api key APIKey options.Option = "api_key" // Location openweathermap location Location options.Option = "location" // Units openweathermap units Units options.Option = "units" // CacheKeyResponse key used when caching the response CacheKeyResponse string = "owm_response" // CacheKeyURL key used when caching the url responsible for the response CacheKeyURL string = "owm_url" ) type weather struct { ShortDescription string `json:"main"` Description string `json:"description"` TypeID string `json:"icon"` } type temperature struct { Value float64 `json:"temp"` } type owmDataResponse struct { Data []weather `json:"weather"` temperature `json:"main"` } func (d *Owm) Enabled() bool { err := d.setStatus() if err != nil { log.Error(err) return false } return true } func (d *Owm) Template() string { return " {{ .Weather }} ({{ .Temperature }}{{ .UnitIcon }}) " } func (d *Owm) getResult() (*owmDataResponse, error) { response := new(owmDataResponse) apikey := d.options.Template(APIKey, "", d) if apikey == "" { return nil, errors.New("no api key found") } location := d.options.Template(Location, "", d) if location == "" { return nil, errors.New("no location found") } location = url.QueryEscape(location) units := d.options.String(Units, "standard") httpTimeout := d.options.Int(options.HTTPTimeout, options.DefaultHTTPTimeout) d.URL = fmt.Sprintf("https://api.openweathermap.org/data/2.5/weather?q=%s&units=%s&appid=%s", location, units, apikey) body, err := d.env.HTTPRequest(d.URL, nil, httpTimeout) if err != nil { return new(owmDataResponse), err } err = json.Unmarshal(body, &response) if err != nil { return new(owmDataResponse), err } return response, nil } func (d *Owm) setStatus() error { units := d.options.String(Units, "standard") q, err := d.getResult() if err != nil { return err } if len(q.Data) == 0 { return errors.New("no data found") } id := q.Data[0].TypeID d.Temperature = int(math.Round(q.Value)) icon := "" switch id { case "01n": icon = "\ue32b" case "01d": icon = "\ue30d" case "02n": icon = "\ue37e" case "02d": icon = "\ue302" case "03n": fallthrough case "03d": icon = "\ue33d" case "04n": fallthrough case "04d": icon = "\ue312" case "09n": fallthrough case "09d": icon = "\ue319" case "10n": icon = "\ue325" case "10d": icon = "\ue308" case "11n": icon = "\ue32a" case "11d": icon = "\ue30f" case "13n": fallthrough case "13d": icon = "\ue31a" case "50n": fallthrough case "50d": icon = "\ue313" } d.Weather = icon d.units = units d.UnitIcon = "\ue33e" switch d.units { case "imperial": d.UnitIcon = "°F" // \ue341" case "metric": d.UnitIcon = "°C" // \ue339" case "": fallthrough case "standard": d.UnitIcon = "°K" // K" } return nil } ================================================ FILE: src/segments/owm_test.go ================================================ package segments import ( "errors" "fmt" "net/url" "testing" "github.com/jandedobbeleer/oh-my-posh/src/runtime/mock" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" "github.com/stretchr/testify/assert" ) const ( OWMWEATHERAPIURL = "https://api.openweathermap.org/data/2.5/weather?q=%s&units=metric&appid=key" ) func TestOWMSegmentSingle(t *testing.T) { cases := []struct { Error error Case string Location string WeatherJSONResponse string ExpectedString string Template string ExpectedEnabled bool }{ { Case: "Sunny Display", Location: "AMSTERDAM,NL", WeatherJSONResponse: `{"weather":[{"icon":"01d"}],"main":{"temp":20}}`, ExpectedString: "\ue30d (20°C)", ExpectedEnabled: true, }, { Case: "Sunny Display", Location: "AMSTERDAM,NL", WeatherJSONResponse: `{"weather":[{"icon":"01d"}],"main":{"temp":20}}`, ExpectedString: "\ue30d (20°C)", ExpectedEnabled: true, Template: "{{.Weather}} ({{.Temperature}}{{.UnitIcon}})", }, { Case: "Sunny Display", Location: "AMSTERDAM,NL", WeatherJSONResponse: `{"weather":[{"icon":"01d"}],"main":{"temp":20}}`, ExpectedString: "\ue30d", ExpectedEnabled: true, Template: "{{.Weather}} ", }, { Case: "Config Skip Geocoding Check With Location", Location: "AMSTERDAM,NL", WeatherJSONResponse: `{"weather":[{"icon":"01d"}],"main":{"temp":20}}`, ExpectedString: "\ue30d (20°C)", ExpectedEnabled: true, }, { Case: "Config Skip Geocoding Check Without Location", WeatherJSONResponse: `{"weather":[{"icon":"01d"}],"main":{"temp":20}}`, ExpectedEnabled: false, }, { Case: "Error in retrieving data", Location: "AMSTERDAM,NL", WeatherJSONResponse: "nonsense", Error: errors.New("Something went wrong"), ExpectedEnabled: false, }, } for _, tc := range cases { env := &mock.Environment{} props := options.Map{ APIKey: "key", Location: tc.Location, Units: "metric", } location := url.QueryEscape(tc.Location) testURL := fmt.Sprintf(OWMWEATHERAPIURL, location) env.On("HTTPRequest", testURL).Return([]byte(tc.WeatherJSONResponse), tc.Error) o := &Owm{} o.Init(props, env) enabled := o.Enabled() assert.Equal(t, tc.ExpectedEnabled, enabled, tc.Case) if !enabled { continue } if tc.Template == "" { tc.Template = o.Template() } assert.Equal(t, tc.ExpectedString, renderTemplate(env, tc.Template, o), tc.Case) } } func TestOWMSegmentIcons(t *testing.T) { cases := []struct { Case string IconID string ExpectedIconString string }{ { Case: "Sunny Display day", IconID: "01d", ExpectedIconString: "\ue30d", }, { Case: "Light clouds Display day", IconID: "02d", ExpectedIconString: "\ue302", }, { Case: "Cloudy Display day", IconID: "03d", ExpectedIconString: "\ue33d", }, { Case: "Broken Clouds Display day", IconID: "04d", ExpectedIconString: "\ue312", }, { Case: "Shower Rain Display day", IconID: "09d", ExpectedIconString: "\ue319", }, { Case: "Rain Display day", IconID: "10d", ExpectedIconString: "\ue308", }, { Case: "Thunderstorm Display day", IconID: "11d", ExpectedIconString: "\ue30f", }, { Case: "Snow Display day", IconID: "13d", ExpectedIconString: "\ue31a", }, { Case: "Fog Display day", IconID: "50d", ExpectedIconString: "\ue313", }, { Case: "Sunny Display night", IconID: "01n", ExpectedIconString: "\ue32b", }, { Case: "Light clouds Display night", IconID: "02n", ExpectedIconString: "\ue37e", }, { Case: "Cloudy Display night", IconID: "03n", ExpectedIconString: "\ue33d", }, { Case: "Broken Clouds Display night", IconID: "04n", ExpectedIconString: "\ue312", }, { Case: "Shower Rain Display night", IconID: "09n", ExpectedIconString: "\ue319", }, { Case: "Rain Display night", IconID: "10n", ExpectedIconString: "\ue325", }, { Case: "Thunderstorm Display night", IconID: "11n", ExpectedIconString: "\ue32a", }, { Case: "Snow Display night", IconID: "13n", ExpectedIconString: "\ue31a", }, { Case: "Fog Display night", IconID: "50n", ExpectedIconString: "\ue313", }, } location := url.QueryEscape("AMSTERDAM,NL") testURL := fmt.Sprintf(OWMWEATHERAPIURL, location) for _, tc := range cases { env := &mock.Environment{} weatherResponse := fmt.Sprintf(`{"weather":[{"icon":"%s"}],"main":{"temp":20.3}}`, tc.IconID) expectedString := fmt.Sprintf("%s (20°C)", tc.ExpectedIconString) env.On("HTTPRequest", testURL).Return([]byte(weatherResponse), nil) props := options.Map{ APIKey: "key", Location: "AMSTERDAM,NL", Units: "metric", } o := &Owm{} o.Init(props, env) assert.Nil(t, o.setStatus()) assert.Equal(t, expectedString, renderTemplate(env, o.Template(), o), tc.Case) } } ================================================ FILE: src/segments/path.go ================================================ package segments import ( "fmt" "sort" "strconv" "strings" "unicode" "unicode/utf8" "github.com/jandedobbeleer/oh-my-posh/src/log" "github.com/jandedobbeleer/oh-my-posh/src/regex" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/runtime/path" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" "github.com/jandedobbeleer/oh-my-posh/src/shell" "github.com/jandedobbeleer/oh-my-posh/src/template" "github.com/jandedobbeleer/oh-my-posh/src/text" ) const ( regexPrefix = "re:" ) type Folder struct { Name string Path string Display bool } type Folders []*Folder func (f Folders) List() []string { var list []string for _, folder := range f { list = append(list, folder.Name) } return list } func (f Folders) Last() *Folder { return f[len(f)-1] } type Path struct { Base mappedLocations map[string]string root string relative string pwd string Location string pathSeparator string Path string Folders Folders StackCount int windowsPath bool Writable bool RootDir bool cygPath bool } const ( // FolderSeparatorIcon the path which is split will be separated by this icon FolderSeparatorIcon options.Option = "folder_separator_icon" // FolderSeparatorTemplate the path which is split will be separated by this template FolderSeparatorTemplate options.Option = "folder_separator_template" // HomeIcon indicates the $HOME location HomeIcon options.Option = "home_icon" // FolderIcon identifies one folder FolderIcon options.Option = "folder_icon" // WindowsRegistryIcon indicates the registry location on Windows WindowsRegistryIcon options.Option = "windows_registry_icon" // Agnoster displays a short path with separator icon, this the default style Agnoster string = "agnoster" // AgnosterFull displays all the folder names with the folder_separator_icon AgnosterFull string = "agnoster_full" // AgnosterShort displays the folder names with one folder_separator_icon, regardless of depth AgnosterShort string = "agnoster_short" // Short displays a shorter path Short string = "short" // Full displays the full path Full string = "full" // FolderType displays the current folder FolderType string = "folder" // Mixed like agnoster, but if a folder name is short enough, it is displayed as-is Mixed string = "mixed" // Letter like agnoster, but with the first letter of each folder name Letter string = "letter" // Unique like agnoster, but with the first unique letters of each folder name Unique string = "unique" // AgnosterLeft like agnoster, but keeps the left side of the path AgnosterLeft string = "agnoster_left" // Powerlevel tries to mimic the powerlevel10k path, // used in combination with max_width. Powerlevel string = "powerlevel" // MixedThreshold the threshold of the length of the path Mixed will display MixedThreshold options.Option = "mixed_threshold" // MappedLocations allows overriding certain location with an icon MappedLocations options.Option = "mapped_locations" // MappedLocationsEnabled enables overriding certain locations with an icon MappedLocationsEnabled options.Option = "mapped_locations_enabled" // MaxDepth Maximum path depth to display without shortening MaxDepth options.Option = "max_depth" // MaxWidth Maximum path width to display for powerlevel style MaxWidth options.Option = "max_width" // Hides the root location if it doesn't fit in max_depth. Used in Agnoster Short HideRootLocation options.Option = "hide_root_location" // A color override cycle Cycle options.Option = "cycle" // Color the path separators within the cycle CycleFolderSeparator options.Option = "cycle_folder_separator" // format to use on the folder names FolderFormat options.Option = "folder_format" // format to use on the first and last folder of the path EdgeFormat options.Option = "edge_format" // format to use on first folder of the path LeftFormat options.Option = "left_format" // format to use on the last folder of the path RightFormat options.Option = "right_format" // GitDirFormat format to use on the git directory GitDirFormat options.Option = "gitdir_format" // DisplayCygpath transforms the path to a cygpath format DisplayCygpath options.Option = "display_cygpath" // DisplayRoot indicates if the linux root slash should be displayed DisplayRoot options.Option = "display_root" // Fish displays the path in a fish-like style Fish string = "fish" // DirLength the length of the directory name to display in fish style DirLength options.Option = "dir_length" // FullLengthDirs indicates how many full length directory names should be displayed in fish style FullLengthDirs options.Option = "full_length_dirs" ) func (pt *Path) Template() string { return " {{ .Path }} " } func (pt *Path) Enabled() bool { pt.setPaths() if pt.pwd == "" { return false } pt.setStyle() pwd := pt.env.Pwd() pt.Location = pt.env.Flags().AbsolutePWD if pt.env.GOOS() == runtime.WINDOWS { pt.Location = strings.ReplaceAll(pt.Location, `\`, `/`) } pt.StackCount = pt.env.StackCount() pt.Writable = pt.env.DirIsWritable(pwd) return true } func (pt *Path) setPaths() { defer func() { pt.Folders = pt.splitPath() }() displayCygpath := func() bool { enableCygpath := pt.options.Bool(DisplayCygpath, false) if !enableCygpath { return false } return pt.env.IsCygwin() } pt.cygPath = displayCygpath() pt.windowsPath = pt.env.GOOS() == runtime.WINDOWS && !pt.cygPath if pt.pathSeparator == "" { pt.pathSeparator = path.Separator() } pt.pwd = pt.env.Pwd() if pt.env.Shell() == shell.PWSH && len(pt.env.Flags().PSWD) != 0 { pt.pwd = pt.env.Flags().PSWD } if pt.pwd == "" { return } // ensure a clean path pt.root, pt.relative = pt.replaceMappedLocations(pt.pwd) pt.pwd = pt.join(pt.root, pt.relative) } func (pt *Path) Parent() string { if pt.pwd == "" { return "" } folders := pt.Folders.List() if len(folders) == 0 { // No parent. return "" } sb := text.NewBuilder() folderSeparator := pt.getFolderSeparator() sb.WriteString(pt.root) if !pt.endWithSeparator(pt.root) { sb.WriteString(folderSeparator) } for _, folder := range folders[:len(folders)-1] { sb.WriteString(folder) sb.WriteString(folderSeparator) } return sb.String() } func (pt *Path) Format(inputPath string) string { separator := path.Separator() elements := strings.Split(inputPath, separator) if len(elements) == 0 { return inputPath } if len(elements) == 1 { return pt.colorizePath(elements[0], nil) } return pt.colorizePath(elements[0], elements[1:]) } func (pt *Path) setStyle() { if pt.relative == "" { root := pt.root // Only append a separator to a non-filesystem PSDrive root or a Windows drive root. if (len(pt.env.Flags().PSWD) != 0 || pt.windowsPath) && strings.HasSuffix(root, ":") { root += pt.getFolderSeparator() } pt.Path = pt.colorizePath(root, nil) return } switch style := pt.options.String(options.Style, Agnoster); style { case Agnoster: maxWidth := pt.getMaxWidth() pt.Path = pt.getAgnosterPath(maxWidth) case AgnosterFull: pt.Path = pt.getAgnosterFullPath() case AgnosterShort: pt.Path = pt.getAgnosterShortPath() case Mixed: pt.Path = pt.getMixedPath() case Letter: pt.Path = pt.getLetterPath() case Unique: pt.Path = pt.getUniqueLettersPath(0) case AgnosterLeft: pt.Path = pt.getAgnosterLeftPath() case Full, Short: // "short" is a duplicate of "full", just here for backwards compatibility pt.Path = pt.getFullPath() case FolderType: pt.Path = pt.getFolderPath() case Powerlevel: maxWidth := pt.getMaxWidth() pt.Path = pt.getUniqueLettersPath(maxWidth) case Fish: pt.Path = pt.getFishPath() default: pt.Path = fmt.Sprintf("Path style: %s is not available", style) } // make sure we resolve all templates if txt, err := template.Render(pt.Path, pt); err == nil { pt.Path = txt } } func (pt *Path) getMaxWidth() int { width := pt.options.String(MaxWidth, "") if width == "" { return 0 } txt, err := template.Render(width, pt) if err != nil { log.Error(err) return 0 } value, err := strconv.Atoi(txt) if err != nil { log.Error(err) return 0 } return value } func (pt *Path) getFolderSeparator() string { separatorTemplate := pt.options.String(FolderSeparatorTemplate, "") if separatorTemplate == "" { separator := pt.options.String(FolderSeparatorIcon, pt.pathSeparator) // if empty, use the default separator if separator == "" { return pt.pathSeparator } return separator } txt, err := template.Render(separatorTemplate, pt) if err != nil { log.Error(err) } if txt == "" { return pt.pathSeparator } return txt } func (pt *Path) getMixedPath() string { threshold := int(pt.options.Float64(MixedThreshold, 4)) folderIcon := pt.options.String(FolderIcon, "..") root, folders := pt.getPaths() var elements []string for i, n := 0, len(folders); i < n; i++ { folderName := folders[i].Name if len(folderName) > threshold && i != n-1 && !folders[i].Display { elements = append(elements, folderIcon) continue } elements = append(elements, folderName) } return pt.colorizePath(root, elements) } func (pt *Path) getAgnosterPath(maxWidth int) string { if maxWidth > 0 { return pt.getAgnosterMaxWidth(maxWidth) } folderIcon := pt.options.String(FolderIcon, "..") root, folders := pt.getPaths() var elements []string for i, n := 0, len(folders); i < n; i++ { if folders[i].Display || i == n-1 { elements = append(elements, folders[i].Name) continue } elements = append(elements, folderIcon) } return pt.colorizePath(root, elements) } func (pt *Path) getAgnosterLeftPath() string { folderIcon := pt.options.String(FolderIcon, "..") root, folders := pt.getPaths() var elements []string if len(folders) == 0 { return pt.colorizePath(root, elements) } elements = append(elements, folders[0].Name) for i, n := 1, len(folders); i < n; i++ { if folders[i].Display { elements = append(elements, folders[i].Name) continue } elements = append(elements, folderIcon) } return pt.colorizePath(root, elements) } func (pt *Path) findFirstLetterOrNumber(txt string) (letter string, index int) { for i, char := range txt { if unicode.IsLetter(char) || unicode.IsNumber(char) { return string(char), i } } return txt, 0 } func (pt *Path) getRelevantLetter(folder *Folder) string { if folder.Display { return folder.Name } letter, index := pt.findFirstLetterOrNumber(folder.Name) if index == 0 { return letter } // handle non-letter characters before the first found letter return folder.Name[0:index] + letter } func (pt *Path) getLetterPath() string { root, folders := pt.getPaths() root = pt.getRelevantLetter(&Folder{Name: root}) var elements []string for i, n := 0, len(folders); i < n; i++ { if folders[i].Display || i == n-1 { elements = append(elements, folders[i].Name) continue } letter := pt.getRelevantLetter(folders[i]) elements = append(elements, letter) } return pt.colorizePath(root, elements) } func (pt *Path) getFishPath() string { root, folders := pt.getPaths() folders = append(Folders{&Folder{Name: root, Display: false}}, folders...) dirLength := pt.options.Int(DirLength, 1) fullLengthDirs := max(pt.options.Int(FullLengthDirs, 1), 1) folderCount := len(folders) stopAt := folderCount - fullLengthDirs var elements []string for i := range folderCount { name := folders[i].Name runeCount := utf8.RuneCountInString(name) if folders[i].Display || dirLength <= 0 || runeCount < dirLength || i >= stopAt { elements = append(elements, name) continue } // Convert string to rune slice to properly handle multi-byte characters runes := []rune(name) elements = append(elements, string(runes[:dirLength])) } if len(elements) == 1 { return pt.colorizePath(elements[0], nil) } return pt.colorizePath(elements[0], elements[1:]) } func (pt *Path) getUniqueLettersPath(maxWidth int) string { dr := pt.options.Bool(DisplayRoot, false) log.Debugf("%t", dr) separator := pt.getFolderSeparator() root, folders := pt.getPaths() folderNames := folders.List() usePowerlevelStyle := func(root, relative string) bool { length := len(root) + len(relative) if !pt.endWithSeparator(root) { length += len(separator) } return length <= maxWidth } if maxWidth > 0 { relative := strings.Join(folderNames, separator) if usePowerlevelStyle(root, relative) { return pt.colorizePath(root, folderNames) } } root = pt.getRelevantLetter(&Folder{Name: root}) var elements []string letters := make(map[string]bool) letters[root] = true for i, n := 0, len(folders); i < n; i++ { folderName := folderNames[i] if i == n-1 { elements = append(elements, folderName) break } letter := pt.getRelevantLetter(folders[i]) for letters[letter] { if letter == folderName { break } letter += folderName[len(letter) : len(letter)+1] } letters[letter] = true elements = append(elements, letter) // only return early on maxWidth > 0 // this enables the powerlevel10k behavior if maxWidth > 0 { list := elements list = append(list, folderNames[i+1:]...) relative := strings.Join(list, separator) if usePowerlevelStyle(root, relative) { return pt.colorizePath(root, list) } } } return pt.colorizePath(root, elements) } func (pt *Path) getAgnosterMaxWidth(maxWidth int) string { separator := pt.getFolderSeparator() folderIcon := pt.options.String(FolderIcon, "..") root, folders := pt.getPaths() folderNames := append([]string{root}, folders.List()...) // this assumes that the root is never a single character // except when it really is / on unix systems if len(root) == 1 { maxWidth++ // add one for the separator } if len(folderNames) == 0 { return pt.colorizePath(root, nil) } fullPath := strings.Join(folderNames, separator) for i := 0; i < len(folderNames)-1 && utf8.RuneCountInString(fullPath) > maxWidth; i++ { folderNames[i] = folderIcon fullPath = strings.Join(folderNames, separator) } for len(folderNames) > 1 && utf8.RuneCountInString(fullPath) > maxWidth { // remove every folder until the path is short enough folderNames = folderNames[1:] fullPath = strings.Join(folderNames, separator) } if len(folderNames) == 1 { return pt.colorizePath(template.TruncE(maxWidth, folderNames[0]), nil) } return pt.colorizePath(folderNames[0], folderNames[1:]) } func (pt *Path) getAgnosterFullPath() string { root, folders := pt.getPaths() return pt.colorizePath(root, folders.List()) } func (pt *Path) getAgnosterShortPath() string { root, folders := pt.getPaths() maxDepth := max(pt.options.Int(MaxDepth, 1), 1) pathDepth := len(folders) hideRootLocation := pt.options.Bool(HideRootLocation, false) folderIcon := pt.options.String(FolderIcon, "..") // No need to shorten. if pathDepth < maxDepth || (pathDepth == maxDepth && !hideRootLocation) { return pt.getAgnosterFullPath() } elements := []string{folderIcon} for i := pathDepth - maxDepth; i < pathDepth; i++ { elements = append(elements, folders[i].Name) } if hideRootLocation { return pt.colorizePath(elements[0], elements[1:]) } return pt.colorizePath(root, elements) } func (pt *Path) getFullPath() string { return pt.colorizePath(pt.root, pt.Folders.List()) } func (pt *Path) getFolderPath() string { folderName := pt.Folders[len(pt.Folders)-1].Name return pt.colorizePath(folderName, nil) } func (pt *Path) join(root, relative string) string { // this is a full replacement of the parent if root == "" { return relative } if !pt.endWithSeparator(root) && len(relative) > 0 { return root + pt.pathSeparator + relative } return root + relative } func (pt *Path) setMappedLocations() { if pt.mappedLocations != nil { return } mappedLocations := make(map[string]string) // predefined mapped locations, can be disabled if pt.options.Bool(MappedLocationsEnabled, true) { mappedLocations["hkcu:"] = pt.options.String(WindowsRegistryIcon, "\uF013") mappedLocations["hklm:"] = pt.options.String(WindowsRegistryIcon, "\uF013") mappedLocations[pt.normalize(pt.env.Home())] = pt.options.String(HomeIcon, "~") } // merge custom locations with mapped locations // mapped locations can override predefined locations keyValues := pt.options.KeyValueMap(MappedLocations, make(map[string]string)) for key, value := range keyValues { if key == "" { continue } location, err := template.Render(key, pt) if err != nil { log.Error(err) } if location == "" { continue } if !strings.HasPrefix(location, regexPrefix) { location = pt.normalize(location) } // When two templates resolve to the same key, the values are compared in ascending order and the latter is taken. if v, exist := mappedLocations[location]; exist && value <= v { continue } mappedLocations[location] = value } pt.mappedLocations = mappedLocations } func (pt *Path) replaceMappedLocations(inputPath string) (string, string) { root, relative := pt.parsePath(inputPath) if relative == "" { pt.RootDir = true } pt.setMappedLocations() if len(pt.mappedLocations) == 0 { return root, relative } // sort map keys in reverse order // fixes case when a subfoder and its parent are mapped // ex /users/test and /users/test/dev keys := make([]string, 0, len(pt.mappedLocations)) for k := range pt.mappedLocations { keys = append(keys, k) } sort.Sort(sort.Reverse(sort.StringSlice(keys))) rootN := pt.normalize(root) relativeN := pt.normalize(relative) escape := func(path string) string { // Escape chevron characters to avoid applying unexpected text styles. return strings.NewReplacer("<", "<<>", ">", "<>>").Replace(path) } handleRegex := func(key string) (string, bool) { if !strings.HasPrefix(key, regexPrefix) { return "", false } input := strings.ReplaceAll(inputPath, `\`, `/`) pattern := key[len(regexPrefix):] // Add (?i) at the start of the pattern for case-insensitive matching on Windows if pt.windowsPath || (pt.env.IsWsl() && strings.HasPrefix(input, "/mnt/")) { pattern = "(?i)" + pattern } match, OK := regex.FindStringMatch(pattern, input, 1) if !OK { return "", false } // Replace the first match with the mapped location. input = strings.Replace(input, match, pt.mappedLocations[key], 1) input = path.Clean(input) return input, true } for _, key := range keys { if input, OK := handleRegex(key); OK { return pt.parsePath(input) } keyRoot, keyRelative := pt.parsePath(key) matchSubFolders := strings.HasSuffix(keyRelative, pt.pathSeparator+"*") if matchSubFolders { // Remove the trailing wildcard (*). keyRelative = keyRelative[:len(keyRelative)-1] } if keyRoot != rootN || !strings.HasPrefix(relativeN, keyRelative) { continue } value := pt.mappedLocations[key] overflow := relative[len(keyRelative):] // exactly match the full path if overflow == "" { return value, "" } // only match the root if keyRelative == "" { return value, strings.Trim(escape(relative), pt.pathSeparator) } // match several prefix elements if matchSubFolders || overflow[:1] == pt.pathSeparator { return value, strings.Trim(escape(overflow), pt.pathSeparator) } } return escape(root), strings.Trim(escape(relative), pt.pathSeparator) } // parsePath parses a clean input path into a root and a relative. func (pt *Path) parsePath(inputPath string) (string, string) { var root, relative string if inputPath == "" { return root, relative } if pt.cygPath { cygPath, err := pt.env.RunCommand("cygpath", "-u", inputPath) if len(cygPath) != 0 { inputPath = cygPath pt.pathSeparator = "/" } if err != nil { pt.cygPath = false pt.windowsPath = true } } if pt.env.GOOS() == runtime.WINDOWS { // Handle a UNC path, if any. pattern := fmt.Sprintf(`^\%[1]s{2}(?P[^\%[1]s]+)\%[1]s(?P[^\%[1]s]+)(\%[1]s(?P[\s\S]*))?$`, pt.pathSeparator) matches := regex.FindNamedRegexMatch(pattern, inputPath) if len(matches) > 0 { root = fmt.Sprintf(`%[1]s%[1]s%[2]s%[1]s%[3]s`, pt.pathSeparator, matches["hostname"], matches["sharename"]) relative = matches["path"] return root, relative } } s := strings.SplitAfterN(inputPath, pt.pathSeparator, 2) root = s[0] if len(s) == 2 { if len(root) > 1 { root = root[:len(root)-1] } relative = s[1] } return root, relative } func (pt *Path) getPaths() (string, Folders) { root := pt.root folders := pt.Folders isRootFS := func(inputPath string) bool { displayRoot := pt.options.Bool(DisplayRoot, false) if displayRoot { return false } return len(inputPath) == 1 && path.IsSeparator(inputPath[0]) } if isRootFS(root) && len(folders) > 0 { root = folders[0].Name folders = folders[1:] } return root, folders } func (pt *Path) endWithSeparator(inputPath string) bool { if inputPath == "" { return false } return path.IsSeparator(inputPath[len(inputPath)-1]) } func (pt *Path) normalize(inputPath string) string { normalized := inputPath if strings.HasPrefix(normalized, "~") && (len(normalized) == 1 || path.IsSeparator(normalized[1])) { normalized = pt.env.Home() + normalized[1:] } normalized = path.Clean(normalized) if pt.env.GOOS() == runtime.WINDOWS || pt.env.GOOS() == runtime.DARWIN { normalized = strings.ToLower(normalized) } if pt.cygPath { return strings.ReplaceAll(normalized, `\`, "/") } return normalized } func (pt *Path) colorizePath(root string, elements []string) string { cycle := pt.options.StringArray(Cycle, []string{}) skipColorize := len(cycle) == 0 folderSeparator := pt.getFolderSeparator() colorSeparator := pt.options.Bool(CycleFolderSeparator, false) folderFormat := pt.options.String(FolderFormat, "%s") edgeFormat := pt.options.String(EdgeFormat, folderFormat) leftFormat := pt.options.String(LeftFormat, edgeFormat) rightFormat := pt.options.String(RightFormat, edgeFormat) colorizeElement := func(element string) string { if skipColorize || element == "" { return element } defer func() { cycle = append(cycle[1:], cycle[0]) }() return fmt.Sprintf("<%s>%s", cycle[0], element) } if len(elements) == 0 { formattedRoot := fmt.Sprintf(leftFormat, root) return colorizeElement(formattedRoot) } colorizeSeparator := func() string { if skipColorize || !colorSeparator { return folderSeparator } return fmt.Sprintf("<%s>%s", cycle[0], folderSeparator) } // Pre-calculate total capacity needed totalLen := len(root) for _, el := range elements { totalLen += len(el) + 20 // estimate for color codes } sb := text.NewBuilder() sb.Grow(totalLen) formattedRoot := fmt.Sprintf(leftFormat, root) sb.WriteString(colorizeElement(formattedRoot)) if !pt.endWithSeparator(root) { sb.WriteString(colorizeSeparator()) } for i, element := range elements { if element == "" { continue } format := folderFormat if i == len(elements)-1 { format = rightFormat } formattedElement := fmt.Sprintf(format, element) sb.WriteString(colorizeElement(formattedElement)) if i != len(elements)-1 { sb.WriteString(colorizeSeparator()) } } return sb.String() } func (pt *Path) splitPath() Folders { folders := Folders{} if pt.relative == "" { return folders } elements := strings.SplitSeq(pt.relative, pt.pathSeparator) folderFormatMap := pt.makeFolderFormatMap() currentPath := pt.root if !pt.endWithSeparator(pt.root) { currentPath += pt.pathSeparator } var display bool for element := range elements { currentPath += element if format := folderFormatMap[currentPath]; len(format) != 0 { element = fmt.Sprintf(format, element) display = true } folders = append(folders, &Folder{Name: element, Path: currentPath, Display: display}) currentPath += pt.pathSeparator display = false } return folders } func (pt *Path) makeFolderFormatMap() map[string]string { folderFormatMap := make(map[string]string) if gitDirFormat := pt.options.String(GitDirFormat, ""); len(gitDirFormat) != 0 { dir, err := pt.env.HasParentFilePath(".git", false) if err == nil && dir.IsDir { // Make it consistent with the modified parent. parent := pt.join(pt.replaceMappedLocations(dir.ParentFolder)) folderFormatMap[parent] = gitDirFormat } } return folderFormatMap } ================================================ FILE: src/segments/path_test.go ================================================ package segments import ( "strings" "testing" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/runtime/mock" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" "github.com/jandedobbeleer/oh-my-posh/src/shell" "github.com/jandedobbeleer/oh-my-posh/src/template" "github.com/stretchr/testify/assert" testify_ "github.com/stretchr/testify/mock" ) const ( homeDir = "/home/someone" homeDirWindows = "C:\\Users\\someone" ) func renderTemplateNoTrimSpace(env *mock.Environment, segmentTemplate string, context any) string { env.On("Shell").Return("foo") if template.Cache == nil { template.Cache = &cache.Template{} } template.Init(env, nil, nil) text, err := template.Render(segmentTemplate, context) if err != nil { return err.Error() } return text } func renderTemplate(env *mock.Environment, segmentTemplate string, context any) string { return strings.TrimSpace(renderTemplateNoTrimSpace(env, segmentTemplate, context)) } type testParentCase struct { Case string Expected string HomePath string Pwd string GOOS string PathSeparator string FolderSeparatorIcon string } func TestParent(t *testing.T) { for _, tc := range testParentCases { env := new(mock.Environment) env.On("Home").Return(tc.HomePath) env.On("Pwd").Return(tc.Pwd) env.On("Flags").Return(&runtime.Flags{}) env.On("Shell").Return(shell.GENERIC) env.On("PathSeparator").Return(tc.PathSeparator) env.On("GOOS").Return(tc.GOOS) props := options.Map{ FolderSeparatorIcon: tc.FolderSeparatorIcon, } path := &Path{} path.Init(props, env) path.setPaths() got := path.Parent() assert.EqualValues(t, tc.Expected, got, tc.Case) } } type testAgnosterPathStyleCase struct { CygpathError error GOOS string Shell string Pswd string Pwd string PathSeparator string HomeIcon string HomePath string Style string FolderSeparatorIcon string Cygpath string Expected string MaxDepth int MaxWidth int HideRootLocation bool Cygwin bool DisplayRoot bool } func TestAgnosterPathStyles(t *testing.T) { for _, tc := range testAgnosterPathStyleCases { env := new(mock.Environment) env.On("PathSeparator").Return(tc.PathSeparator) env.On("Home").Return(tc.HomePath) env.On("Pwd").Return(tc.Pwd) env.On("GOOS").Return(tc.GOOS) env.On("IsCygwin").Return(tc.Cygwin) env.On("StackCount").Return(0) env.On("IsWsl").Return(false) args := &runtime.Flags{ PSWD: tc.Pswd, } env.On("Flags").Return(args) if tc.Shell == "" { tc.Shell = shell.PWSH } env.On("Shell").Return(tc.Shell) displayCygpath := tc.Cygwin if displayCygpath { env.On("RunCommand", "cygpath", []string{"-u", tc.Pwd}).Return(tc.Cygpath, tc.CygpathError) env.On("RunCommand", "cygpath", testify_.Anything).Return("brrrr", nil) } props := options.Map{ FolderSeparatorIcon: tc.FolderSeparatorIcon, options.Style: tc.Style, MaxDepth: tc.MaxDepth, MaxWidth: tc.MaxWidth, HideRootLocation: tc.HideRootLocation, DisplayCygpath: displayCygpath, DisplayRoot: tc.DisplayRoot, } path := &Path{} path.Init(props, env) path.setPaths() path.setStyle() got := renderTemplateNoTrimSpace(env, "{{ .Path }}", path) assert.Equal(t, tc.Expected, got) } } type testFullAndFolderPathCase struct { Style string HomePath string FolderSeparatorIcon string Pwd string Pswd string Expected string GOOS string PathSeparator string Template string StackCount int DisableMappedLocations bool } func TestFullAndFolderPath(t *testing.T) { for _, tc := range testFullAndFolderPathCases { env := new(mock.Environment) if tc.PathSeparator == "" { tc.PathSeparator = "/" } env.On("PathSeparator").Return(tc.PathSeparator) if tc.GOOS == runtime.WINDOWS { env.On("Home").Return(homeDirWindows) } else { env.On("Home").Return(homeDir) } env.On("Pwd").Return(tc.Pwd) env.On("GOOS").Return(tc.GOOS) env.On("StackCount").Return(tc.StackCount) env.On("IsWsl").Return(false) args := &runtime.Flags{ PSWD: tc.Pswd, } env.On("Flags").Return(args) env.On("Shell").Return(shell.GENERIC) if tc.Template == "" { tc.Template = "{{ if gt .StackCount 0 }}{{ .StackCount }} {{ end }}{{ .Path }}" } props := options.Map{ options.Style: tc.Style, } if tc.FolderSeparatorIcon != "" { props[FolderSeparatorIcon] = tc.FolderSeparatorIcon } if tc.DisableMappedLocations { props[MappedLocationsEnabled] = false } path := &Path{ StackCount: env.StackCount(), } path.Init(props, env) path.setPaths() path.setStyle() got := renderTemplateNoTrimSpace(env, tc.Template, path) assert.Equal(t, tc.Expected, got) } } type testFullPathCustomMappedLocationsCase struct { Pwd string MappedLocations map[string]string GOOS string PathSeparator string Expected string } func TestFullPathCustomMappedLocations(t *testing.T) { for _, tc := range testFullPathCustomMappedLocationsCases { env := new(mock.Environment) env.On("Home").Return(homeDir) env.On("Pwd").Return(tc.Pwd) if tc.GOOS == "" { tc.GOOS = runtime.DARWIN } env.On("GOOS").Return(tc.GOOS) if tc.PathSeparator == "" { tc.PathSeparator = "/" } env.On("PathSeparator").Return(tc.PathSeparator) args := &runtime.Flags{ PSWD: tc.Pwd, } env.On("Flags").Return(args) env.On("Shell").Return(shell.GENERIC) env.On("Getenv", "HOME").Return(homeDir) template.Cache = new(cache.Template) template.Init(env, nil, nil) props := options.Map{ options.Style: Full, MappedLocationsEnabled: false, MappedLocations: tc.MappedLocations, } path := &Path{} path.Init(props, env) path.setPaths() path.setStyle() got := renderTemplateNoTrimSpace(env, "{{ .Path }}", path) assert.Equal(t, tc.Expected, got) } } type testAgnosterPathCase struct { Case string Expected string Home string PWD string GOOS string PathSeparator string Cycle []string ColorSeparator bool } func TestAgnosterPath(t *testing.T) { for _, tc := range testAgnosterPathCases { env := new(mock.Environment) env.On("Home").Return(tc.Home) env.On("PathSeparator").Return(tc.PathSeparator) env.On("Pwd").Return(tc.PWD) env.On("GOOS").Return(tc.GOOS) args := &runtime.Flags{ PSWD: tc.PWD, } env.On("Flags").Return(args) env.On("Shell").Return(shell.PWSH) props := options.Map{ options.Style: Agnoster, FolderSeparatorIcon: " > ", FolderIcon: "f", HomeIcon: "~", Cycle: tc.Cycle, CycleFolderSeparator: tc.ColorSeparator, } path := &Path{} path.Init(props, env) path.setPaths() path.setStyle() got := renderTemplateNoTrimSpace(env, "{{ .Path }}", path) assert.Equal(t, tc.Expected, got, tc.Case) } } type testAgnosterLeftPathCase struct { Case string Expected string Home string PWD string GOOS string PathSeparator string } func TestAgnosterLeftPath(t *testing.T) { for _, tc := range testAgnosterLeftPathCases { env := new(mock.Environment) env.On("Home").Return(tc.Home) env.On("PathSeparator").Return(tc.PathSeparator) env.On("Pwd").Return(tc.PWD) env.On("GOOS").Return(tc.GOOS) args := &runtime.Flags{ PSWD: tc.PWD, } env.On("Flags").Return(args) env.On("Shell").Return(shell.PWSH) props := options.Map{ options.Style: AgnosterLeft, FolderSeparatorIcon: " > ", FolderIcon: "f", HomeIcon: "~", } path := &Path{} path.Init(props, env) path.setPaths() path.setStyle() got := renderTemplateNoTrimSpace(env, "{{ .Path }}", path) assert.Equal(t, tc.Expected, got, tc.Case) } } func TestGetFolderSeparator(t *testing.T) { cases := []struct { Case string FolderSeparatorIcon string FolderSeparatorTemplate string Expected string }{ {Case: "default", Expected: "/"}, {Case: "icon - no template", FolderSeparatorIcon: "\ue5fe", Expected: "\ue5fe"}, {Case: "template", FolderSeparatorTemplate: "{{ if eq .Shell \"bash\" }}\\{{ end }}", Expected: "\\"}, {Case: "template empty", FolderSeparatorTemplate: "{{ if eq .Shell \"pwsh\" }}\\{{ end }}", Expected: "/"}, {Case: "invalid template", FolderSeparatorTemplate: "{{ if eq .Shell \"pwsh\" }}", Expected: "/"}, } for _, tc := range cases { env := new(mock.Environment) env.On("Shell").Return(shell.GENERIC) template.Cache = &cache.Template{ SimpleTemplate: cache.SimpleTemplate{ Shell: "bash", }, } template.Init(env, nil, nil) props := options.Map{} if len(tc.FolderSeparatorTemplate) > 0 { props[FolderSeparatorTemplate] = tc.FolderSeparatorTemplate } if len(tc.FolderSeparatorIcon) > 0 { props[FolderSeparatorIcon] = tc.FolderSeparatorIcon } path := &Path{ pathSeparator: "/", } path.Init(props, env) got := path.getFolderSeparator() assert.Equal(t, tc.Expected, got) } } type testNormalizePathCase struct { Case string Input string HomeDir string GOOS string PathSeparator string Expected string Cygwin bool } func TestNormalizePath(t *testing.T) { for _, tc := range testNormalizePathCases { env := new(mock.Environment) env.On("Home").Return(tc.HomeDir) env.On("GOOS").Return(tc.GOOS) if tc.PathSeparator == "" { tc.PathSeparator = "/" } env.On("PathSeparator").Return(tc.PathSeparator) pt := &Path{cygPath: tc.Cygwin} pt.Init(options.Map{}, env) got := pt.normalize(tc.Input) assert.Equal(t, tc.Expected, got, tc.Case) } } type testSplitPathCase struct { Case string GOOS string Relative string Root string GitDir *runtime.FileInfo GitDirFormat string Expected Folders } func TestSplitPath(t *testing.T) { for _, tc := range testSplitPathCases { env := new(mock.Environment) env.On("PathSeparator").Return("/") env.On("Home").Return("/a/b") env.On("HasParentFilePath", ".git", false).Return(tc.GitDir, nil) env.On("GOOS").Return(tc.GOOS) props := options.Map{ GitDirFormat: tc.GitDirFormat, } path := &Path{ root: tc.Root, relative: tc.Relative, pathSeparator: "/", windowsPath: tc.GOOS == runtime.WINDOWS, } path.Init(props, env) got := path.splitPath() assert.Equal(t, tc.Expected, got, tc.Case) } } func TestGetMaxWidth(t *testing.T) { cases := []struct { MaxWidth any Case string Expected int }{ { Case: "Nil", Expected: 0, }, { Case: "Empty string", MaxWidth: "", Expected: 0, }, { Case: "Invalid template", MaxWidth: "{{ .Unknown }}", Expected: 0, }, { Case: "Environment variable", MaxWidth: "{{ .Env.MAX_WIDTH }}", Expected: 120, }, } for _, tc := range cases { env := new(mock.Environment) env.On("Getenv", "MAX_WIDTH").Return("120") env.On("Shell").Return(shell.BASH) template.Cache = new(cache.Template) template.Init(env, nil, nil) props := options.Map{ MaxWidth: tc.MaxWidth, } path := &Path{} path.Init(props, env) got := path.getMaxWidth() assert.Equal(t, tc.Expected, got, tc.Case) } } func TestAgnosterMaxWidth(t *testing.T) { cases := []struct { name string pwd string folderIcon string separator string expected string goos string maxWidth int displayRoot bool }{ { name: "path shorter than maxWidth", pwd: "/foob/user/docs", maxWidth: 20, displayRoot: false, separator: "/", folderIcon: `..`, expected: "foob/user/docs", goos: runtime.LINUX, }, { name: "path shorter than maxWidth, Windows", pwd: `C:\Users\john\Documents`, maxWidth: 20, displayRoot: true, folderIcon: `..`, separator: `\`, expected: `..\..\john\Documents`, goos: runtime.WINDOWS, }, { name: "path shorter than maxWidth, wth root", pwd: "/foob/user/docs", maxWidth: 20, displayRoot: true, folderIcon: `..`, separator: "/", expected: "/foob/user/docs", goos: runtime.LINUX, }, { name: "path exactly maxWidth", pwd: "/foob/user/docs", maxWidth: 15, displayRoot: true, folderIcon: `..`, separator: "/", expected: "/foob/user/docs", goos: runtime.LINUX, }, { name: "path longer than maxWidth with folder icons", pwd: "/foob/user/documents/projects", maxWidth: 15, displayRoot: false, folderIcon: "..", separator: "/", expected: "../../projects", goos: runtime.LINUX, }, { name: "very long path requiring multiple folder replacements", pwd: "/foob/user/documents/projects/myproject/src/main", maxWidth: 21, displayRoot: false, folderIcon: "..", separator: "/", expected: "../../../../../main", goos: runtime.LINUX, }, { name: "path requiring final folder truncation", pwd: "/foob/verylongfoldername", maxWidth: 15, displayRoot: false, separator: "/", expected: "verylongfolder…", goos: runtime.LINUX, }, { name: "Windows path with custom separator", pwd: `C:\Users\john\Documents`, maxWidth: 15, displayRoot: false, folderIcon: "…", separator: `\`, expected: `…\…\…\Documents`, goos: runtime.WINDOWS, }, { name: "single folder path", pwd: "/foob", maxWidth: 10, displayRoot: false, separator: "/", expected: "foob", goos: runtime.LINUX, }, { name: "empty relative path", pwd: "/", maxWidth: 10, displayRoot: true, separator: "/", expected: "/", goos: runtime.LINUX, }, { name: "custom folder icon", pwd: "/foob/user/documents/projects", maxWidth: 15, displayRoot: false, folderIcon: "⋯", separator: "/", expected: "⋯/⋯/⋯/projects", goos: runtime.LINUX, }, { name: "maxwidth is smaller than folder name", pwd: "/foob/user/documents/projects", maxWidth: 2, displayRoot: false, folderIcon: "⋯", separator: "/", expected: "p…", goos: runtime.LINUX, }, { name: "maxwidth is 0", pwd: "/foob/user/documents/projects", maxWidth: 0, displayRoot: false, folderIcon: "⋯", separator: "/", expected: "…", goos: runtime.LINUX, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { env := &mock.Environment{} env.On("Pwd").Return(tc.pwd) env.On("Home").Return("/home") env.On("GOOS").Return(tc.goos) env.On("Shell").Return(shell.BASH) path := &Path{ Base: Base{ env: env, options: options.Map{ DisplayRoot: tc.displayRoot, FolderIcon: tc.folderIcon, FolderSeparatorIcon: tc.separator, }, }, pathSeparator: tc.separator, } // Set up the path state path.setPaths() got := path.getAgnosterMaxWidth(tc.maxWidth) assert.Equal(t, tc.expected, got, tc.name) }) } } func TestFishPath(t *testing.T) { cases := []struct { name string pwd string separator string goos string expected string dirLength int fullLengthDirs int }{ { name: "default settings", pwd: "/home/user/documents/projects", dirLength: 1, fullLengthDirs: 1, expected: "h/u/d/projects", separator: "/", }, { name: "dir length 2", pwd: "/home/user/documents/projects", dirLength: 2, fullLengthDirs: 1, expected: "ho/us/do/projects", separator: "/", }, { name: "full length dirs 2", pwd: "/home/user/documents/projects/myproject", dirLength: 1, fullLengthDirs: 2, expected: "h/u/d/projects/myproject", separator: "/", }, { name: "dir length 3, full length dirs 2", pwd: "/home/user/documents/projects/myproject", dirLength: 3, fullLengthDirs: 2, expected: "hom/use/doc/projects/myproject", separator: "/", }, { name: "full length dirs 2 - Windows", pwd: `C:\Users\Jan\Documents\Projects\Myproject`, dirLength: 1, fullLengthDirs: 2, expected: `C\U\J\D\Projects\Myproject`, separator: `\`, }, { name: "dir length 3, full length dirs 2 - Windows", pwd: `C:\Users\Jan\Documents\Projects\Myproject`, dirLength: 3, fullLengthDirs: 2, expected: `C:\Use\Jan\Doc\Projects\Myproject`, separator: `\`, }, { name: "single folder", pwd: "/home", dirLength: 1, fullLengthDirs: 1, expected: "home", separator: "/", }, { name: "two folders with full length dirs 1", pwd: "/home/user", dirLength: 1, fullLengthDirs: 1, expected: "h/user", separator: "/", }, { name: "root only", pwd: "/", dirLength: 1, fullLengthDirs: 1, expected: "/", separator: "/", }, { name: "dir length 0 should disable shortening", pwd: "/home/user/documents", dirLength: 0, fullLengthDirs: 1, expected: "home/user/documents", separator: "/", }, { name: "dir length negative should disable shortening", pwd: "/home/user/documents", dirLength: -1, fullLengthDirs: 1, expected: "home/user/documents", separator: "/", }, { name: "full length dirs 0 should fallback to 1", pwd: "/home/user/documents", dirLength: 1, fullLengthDirs: 0, expected: "h/u/documents", separator: "/", }, { name: "full length dirs negative should fallback to 1", pwd: "/home/user/documents", dirLength: 1, fullLengthDirs: -1, expected: "h/u/documents", separator: "/", }, { name: "full length dirs greater than total folders", pwd: "/home/user", dirLength: 1, fullLengthDirs: 5, expected: "home/user", separator: "/", }, { name: "dir length greater than folder name", pwd: "/a/b/c", dirLength: 10, fullLengthDirs: 1, expected: "a/b/c", separator: "/", }, { name: "multi-byte unicode home icon", pwd: "/󰋜/Downloads/test", dirLength: 1, fullLengthDirs: 1, expected: "󰋜/D/test", separator: "/", }, { name: "multi-byte unicode home icon with dir length 2", pwd: "/󰋜/Documents/Projects", dirLength: 2, fullLengthDirs: 1, expected: "󰋜/Do/Projects", separator: "/", }, { name: "path with emoji folders", pwd: "/🏠/📁/💻", dirLength: 1, fullLengthDirs: 1, expected: "🏠/📁/💻", separator: "/", }, { name: "mixed multi-byte and ascii", pwd: "/󰋜test/normal/󰨳end", dirLength: 2, fullLengthDirs: 1, expected: "󰋜t/no/󰨳end", separator: "/", }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { env := &mock.Environment{} env.On("Pwd").Return(tc.pwd) env.On("Home").Return("/foob") env.On("GOOS").Return(tc.goos) env.On("Shell").Return(shell.BASH) path := &Path{ Base: Base{ env: env, options: options.Map{ DirLength: tc.dirLength, FullLengthDirs: tc.fullLengthDirs, }, }, pathSeparator: tc.separator, } path.setPaths() result := path.getFishPath() assert.Equal(t, result, tc.expected, tc.name) }) } } ================================================ FILE: src/segments/path_unix_test.go ================================================ //go:build !windows package segments import ( "testing" "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/runtime/mock" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" "github.com/jandedobbeleer/oh-my-posh/src/shell" "github.com/jandedobbeleer/oh-my-posh/src/template" "github.com/stretchr/testify/assert" ) const ( abc = "/abc" abcd = "/a/b/c/d" cdefg = "/c/d/e/f/g" ) var testParentCases = []testParentCase{ { Case: "Inside Home folder", Expected: "~/", HomePath: homeDir, Pwd: homeDir + "/test", GOOS: runtime.DARWIN, PathSeparator: "/", }, { Case: "Home folder", HomePath: homeDir, Pwd: homeDir, GOOS: runtime.DARWIN, PathSeparator: "/", }, { Case: "Home folder with a trailing separator", HomePath: homeDir, Pwd: homeDir + "/", GOOS: runtime.DARWIN, PathSeparator: "/", }, { Case: "Root", HomePath: homeDir, Pwd: "/", GOOS: runtime.DARWIN, PathSeparator: "/", }, { Case: "Root + 1", Expected: "/", HomePath: homeDir, Pwd: "/usr", GOOS: runtime.DARWIN, PathSeparator: "/", }, } var testAgnosterPathStyleCases = []testAgnosterPathStyleCase{ { Style: Unique, Expected: "~ > a > ab > abcd", HomePath: homeDir, Pwd: homeDir + "/ab/abc/abcd", PathSeparator: "/", FolderSeparatorIcon: " > ", }, { Style: Unique, Expected: "~ > a > .a > abcd", HomePath: homeDir, Pwd: homeDir + "/ab/.abc/abcd", PathSeparator: "/", FolderSeparatorIcon: " > ", }, { Style: Unique, Expected: "~ > a > ab > abcd", HomePath: homeDir, Pwd: homeDir + "/ab/ab/abcd", PathSeparator: "/", FolderSeparatorIcon: " > ", }, { Style: Unique, Expected: "a", HomePath: homeDir, Pwd: "/ab", PathSeparator: "/", FolderSeparatorIcon: " > ", }, { Style: Unique, Expected: "/a > c > ef", HomePath: homeDir, Pwd: "/ab/cd/ef", PathSeparator: "/", FolderSeparatorIcon: " > ", DisplayRoot: true, }, { Style: Powerlevel, Expected: "t > w > o > a > v > l > p > wh > we > i > wa > th > the > d > f > u > it > c > to > a > co > stream", HomePath: homeDir, Pwd: "/there/was/once/a/very/long/path/which/wended/its/way/through/the/dark/forest/until/it/came/to/a/cold/stream", PathSeparator: "/", FolderSeparatorIcon: " > ", MaxWidth: 20, }, { Style: Powerlevel, Expected: "t > w > o > a > v > l > p > which > wended > its > way > through > the", HomePath: homeDir, Pwd: "/there/was/once/a/very/long/path/which/wended/its/way/through/the", PathSeparator: "/", FolderSeparatorIcon: " > ", MaxWidth: 70, }, { Style: Powerlevel, Expected: "var/cache/pacman", HomePath: homeDir, Pwd: "/var/cache/pacman", PathSeparator: "/", FolderSeparatorIcon: "/", MaxWidth: 50, }, { Style: Powerlevel, Expected: "/var/cache/pacman", HomePath: homeDir, Pwd: "/var/cache/pacman", PathSeparator: "/", FolderSeparatorIcon: "/", MaxWidth: 50, DisplayRoot: true, }, { Style: Letter, Expected: "~", HomePath: homeDir, Pwd: homeDir, PathSeparator: "/", FolderSeparatorIcon: " > ", }, { Style: Letter, Expected: "~ > a > w > man", HomePath: homeDir, Pwd: homeDir + "/ab/whatever/man", PathSeparator: "/", FolderSeparatorIcon: " > ", }, { Style: Letter, Expected: "u > b > a > w > man", HomePath: homeDir, Pwd: "/usr/burp/ab/whatever/man", PathSeparator: "/", FolderSeparatorIcon: " > ", }, { Style: Letter, Expected: "u > .b > a > w > man", HomePath: homeDir, Pwd: "/usr/.burp/ab/whatever/man", PathSeparator: "/", FolderSeparatorIcon: " > ", }, { Style: Letter, Expected: "u > .b > a > .w > man", HomePath: homeDir, Pwd: "/usr/.burp/ab/.whatever/man", PathSeparator: "/", FolderSeparatorIcon: " > ", }, { Style: Letter, Expected: "u > .b > a > ._w > man", HomePath: homeDir, Pwd: "/usr/.burp/ab/._whatever/man", PathSeparator: "/", FolderSeparatorIcon: " > ", }, { Style: Letter, Expected: "u > .ä > ū > .w > man", HomePath: homeDir, Pwd: "/usr/.äufbau/ūmgebung/.whatever/man", PathSeparator: "/", FolderSeparatorIcon: " > ", }, { Style: Letter, Expected: "u > .b > 1 > .w > man", HomePath: homeDir, Pwd: "/usr/.burp/12345/.whatever/man", PathSeparator: "/", FolderSeparatorIcon: " > ", }, { Style: Letter, Expected: "u > .b > 1 > .w > man", HomePath: homeDir, Pwd: "/usr/.burp/12345abc/.whatever/man", PathSeparator: "/", FolderSeparatorIcon: " > ", }, { Style: Letter, Expected: "u > .b > __p > .w > man", HomePath: homeDir, Pwd: "/usr/.burp/__pycache__/.whatever/man", PathSeparator: "/", FolderSeparatorIcon: " > ", }, { Style: Letter, Expected: "➼ > .w > man", HomePath: homeDir, Pwd: "/➼/.whatever/man", PathSeparator: "/", FolderSeparatorIcon: " > ", }, { Style: Letter, Expected: "➼ s > .w > man", HomePath: homeDir, Pwd: "/➼ something/.whatever/man", PathSeparator: "/", FolderSeparatorIcon: " > ", }, { Style: Letter, Expected: "w", HomePath: homeDir, Pwd: "/whatever", PathSeparator: "/", FolderSeparatorIcon: " > ", }, { Style: Mixed, Expected: "~ > .. > man", HomePath: homeDir, Pwd: homeDir + "/whatever/man", PathSeparator: "/", FolderSeparatorIcon: " > ", }, { Style: Mixed, Expected: "~ > ab > .. > man", HomePath: homeDir, Pwd: homeDir + "/ab/whatever/man", PathSeparator: "/", FolderSeparatorIcon: " > ", }, { Style: Mixed, Expected: "usr > foo > bar > .. > man", HomePath: homeDir, Pwd: "/usr/foo/bar/foobar/man", PathSeparator: "/", FolderSeparatorIcon: " > ", }, { Style: Mixed, Expected: "whatever > .. > foo > bar", HomePath: homeDir, Pwd: "/whatever/foobar/foo/bar", PathSeparator: "/", FolderSeparatorIcon: " > ", }, { Style: AgnosterFull, Expected: "usr > location > whatever", HomePath: homeDir, Pwd: "/usr/location/whatever", PathSeparator: "/", FolderSeparatorIcon: " > ", }, { Style: AgnosterFull, Expected: "PSDRIVE: | src", HomePath: homeDir, Pwd: "/foo", Pswd: "PSDRIVE:/src", PathSeparator: "/", FolderSeparatorIcon: " | ", }, { Style: AgnosterShort, Expected: ".. | src | init", HomePath: homeDir, Pwd: "/foo", Pswd: "PSDRIVE:/src/init", PathSeparator: "/", FolderSeparatorIcon: " | ", MaxDepth: 2, HideRootLocation: true, }, { Style: AgnosterShort, Expected: "usr > foo > bar > man", HomePath: homeDir, Pwd: "/usr/foo/bar/man", PathSeparator: "/", FolderSeparatorIcon: " > ", MaxDepth: 3, }, { Style: AgnosterShort, Expected: "PSDRIVE: | src", HomePath: homeDir, Pwd: "/foo", Pswd: "PSDRIVE:/src", PathSeparator: "/", FolderSeparatorIcon: " | ", MaxDepth: 2, HideRootLocation: true, }, { Style: AgnosterShort, Expected: "~ > projects", HomePath: homeDir, Pwd: homeDir + "/projects", PathSeparator: "/", FolderSeparatorIcon: " > ", }, { Style: AgnosterShort, Expected: "~", HomePath: homeDir, Pwd: homeDir, PathSeparator: "/", FolderSeparatorIcon: " > ", MaxDepth: 1, HideRootLocation: true, }, { Style: AgnosterShort, Expected: "usr > .. > bar > man", HomePath: homeDir, Pwd: "/usr/foo/bar/man", PathSeparator: "/", FolderSeparatorIcon: " > ", MaxDepth: 2, }, { Style: AgnosterShort, Expected: "~ > .. > man", HomePath: homeDir, Pwd: homeDir + "/whatever/man", PathSeparator: "/", FolderSeparatorIcon: " > ", }, { Style: AgnosterShort, Expected: "usr > .. > man", HomePath: homeDir, Pwd: "/usr/location/whatever/man", PathSeparator: "/", FolderSeparatorIcon: " > ", }, { Style: AgnosterShort, Expected: "~ > .. > bar > man", HomePath: homeDir, Pwd: homeDir + "/foo/bar/man", PathSeparator: "/", FolderSeparatorIcon: " > ", MaxDepth: 2, }, { Style: AgnosterShort, Expected: "~ > foo > bar > man", HomePath: homeDir, Pwd: homeDir + "/foo/bar/man", PathSeparator: "/", FolderSeparatorIcon: " > ", MaxDepth: 3, }, { Style: AgnosterShort, Expected: "PSDRIVE: | .. | init", HomePath: homeDir, Pwd: "/foo", Pswd: "PSDRIVE:/src/init", PathSeparator: "/", FolderSeparatorIcon: " | ", }, { Style: AgnosterShort, Expected: ".. > foo", HomePath: homeDir, Pwd: homeDir + "/foo", PathSeparator: "/", FolderSeparatorIcon: " > ", MaxDepth: 1, HideRootLocation: true, }, { Style: AgnosterShort, Expected: ".. > bar > man", HomePath: homeDir, Pwd: homeDir + "/bar/man", PathSeparator: "/", FolderSeparatorIcon: " > ", MaxDepth: 2, HideRootLocation: true, }, { Style: AgnosterShort, Expected: ".. > foo > bar > man", HomePath: homeDir, Pwd: "/usr/foo/bar/man", PathSeparator: "/", FolderSeparatorIcon: " > ", MaxDepth: 3, HideRootLocation: true, }, { Style: AgnosterShort, Expected: "~ > foo", HomePath: homeDir, Pwd: homeDir + "/foo", PathSeparator: "/", FolderSeparatorIcon: " > ", MaxDepth: 2, HideRootLocation: true, }, { Style: AgnosterShort, Expected: "~ > foo > bar", HomePath: homeDir, Pwd: homeDir + "/foo/bar", PathSeparator: "/", FolderSeparatorIcon: " > ", MaxDepth: 3, HideRootLocation: true, }, { Style: AgnosterShort, Expected: "C: | ", HomePath: homeDir, Pwd: "/mnt/c", Pswd: "C:", PathSeparator: "/", FolderSeparatorIcon: " | ", MaxDepth: 2, HideRootLocation: true, }, { Style: AgnosterShort, Expected: "~ | space foo", HomePath: homeDir, Pwd: homeDir + "/space foo", PathSeparator: "/", FolderSeparatorIcon: " | ", MaxDepth: 2, HideRootLocation: true, }, { Style: AgnosterShort, Expected: ".. | space foo", HomePath: homeDir, Pwd: homeDir + "/space foo", PathSeparator: "/", FolderSeparatorIcon: " | ", MaxDepth: 1, HideRootLocation: true, }, } var testAgnosterPathCases = []testAgnosterPathCase{ { Case: "Unix outside home", Expected: "mnt > f > f > location", Home: homeDir, PWD: "/mnt/go/test/location", PathSeparator: "/", }, { Case: "Unix inside home", Expected: "~ > f > f > location", Home: homeDir, PWD: homeDir + "/docs/jan/location", PathSeparator: "/", }, { Case: "Unix outside home zero levels", Expected: "mnt > location", Home: homeDir, PWD: "/mnt/location", PathSeparator: "/", }, { Case: "Unix outside home one level", Expected: "mnt > f > location", Home: homeDir, PWD: "/mnt/folder/location", PathSeparator: "/", }, { Case: "Unix, colorize", Expected: "mnt > f > location", Home: homeDir, PWD: "/mnt/folder/location", PathSeparator: "/", Cycle: []string{"blue", "yellow"}, }, { Case: "Unix, colorize with folder separator", Expected: "mnt > f > location", Home: homeDir, PWD: "/mnt/folder/location", PathSeparator: "/", Cycle: []string{"blue", "yellow"}, ColorSeparator: true, }, { Case: "Unix one level", Expected: "mnt", Home: homeDir, PWD: "/mnt", PathSeparator: "/", }, } var testAgnosterLeftPathCases = []testAgnosterLeftPathCase{ { Case: "Unix outside home", Expected: "mnt > go > f > f", Home: homeDir, PWD: "/mnt/go/test/location", PathSeparator: "/", }, { Case: "Unix inside home", Expected: "~ > docs > f > f", Home: homeDir, PWD: homeDir + "/docs/jan/location", PathSeparator: "/", }, { Case: "Unix outside home zero levels", Expected: "mnt > location", Home: homeDir, PWD: "/mnt/location", PathSeparator: "/", }, { Case: "Unix outside home one level", Expected: "mnt > folder > f", Home: homeDir, PWD: "/mnt/folder/location", PathSeparator: "/", }, } var testFullAndFolderPathCases = []testFullAndFolderPathCase{ {Style: Full, FolderSeparatorIcon: "|", Pwd: "/", Expected: "/"}, {Style: Full, Pwd: "/", Expected: "/"}, {Style: Full, Pwd: homeDir, Expected: "~"}, {Style: Full, Pwd: homeDir + abc, Expected: "~/abc"}, {Style: Full, Pwd: homeDir + abc, Expected: homeDir + abc, DisableMappedLocations: true}, {Style: Full, Pwd: abcd, Expected: abcd}, {Style: Full, FolderSeparatorIcon: "|", Pwd: homeDir, Expected: "~"}, {Style: Full, FolderSeparatorIcon: "|", Pwd: homeDir, Expected: "/home|someone", DisableMappedLocations: true}, {Style: Full, FolderSeparatorIcon: "|", Pwd: homeDir + abc, Expected: "~|abc"}, {Style: Full, FolderSeparatorIcon: "|", Pwd: abcd, Expected: "/a|b|c|d"}, {Style: FolderType, Pwd: "/", Expected: "/"}, {Style: FolderType, Pwd: homeDir, Expected: "~"}, {Style: FolderType, Pwd: homeDir, Expected: "someone", DisableMappedLocations: true}, {Style: FolderType, Pwd: homeDir + abc, Expected: "abc"}, {Style: FolderType, Pwd: abcd, Expected: "d"}, {Style: FolderType, FolderSeparatorIcon: "|", Pwd: "/", Expected: "/"}, {Style: FolderType, FolderSeparatorIcon: "|", Pwd: homeDir, Expected: "~"}, {Style: FolderType, FolderSeparatorIcon: "|", Pwd: homeDir, Expected: "someone", DisableMappedLocations: true}, {Style: FolderType, FolderSeparatorIcon: "|", Pwd: homeDir + abc, Expected: "abc"}, {Style: FolderType, FolderSeparatorIcon: "|", Pwd: abcd, Expected: "d"}, // StackCountEnabled=true and StackCount=2 {Style: Full, FolderSeparatorIcon: "|", Pwd: "/", StackCount: 2, Expected: "2 /"}, {Style: Full, Pwd: "/", StackCount: 2, Expected: "2 /"}, {Style: Full, Pwd: homeDir, StackCount: 2, Expected: "2 ~"}, {Style: Full, Pwd: homeDir + abc, StackCount: 2, Expected: "2 ~/abc"}, {Style: Full, Pwd: homeDir + abc, StackCount: 2, Expected: "2 " + homeDir + abc, DisableMappedLocations: true}, {Style: Full, Pwd: abcd, StackCount: 2, Expected: "2 /a/b/c/d"}, // StackCountEnabled=false and StackCount=2 {Style: Full, FolderSeparatorIcon: "|", Pwd: "/", Template: "{{ .Path }}", StackCount: 2, Expected: "/"}, {Style: Full, Pwd: "/", Template: "{{ .Path }}", StackCount: 2, Expected: "/"}, {Style: Full, Pwd: homeDir, Template: "{{ .Path }}", StackCount: 2, Expected: "~"}, {Style: Full, Pwd: homeDir + abc, Template: "{{ .Path }}", StackCount: 2, Expected: homeDir + abc, DisableMappedLocations: true}, {Style: Full, Pwd: abcd, Template: "{{ .Path }}", StackCount: 2, Expected: abcd}, // StackCountEnabled=true and StackCount=0 {Style: Full, FolderSeparatorIcon: "|", Pwd: "/", StackCount: 0, Expected: "/"}, {Style: Full, Pwd: "/", StackCount: 0, Expected: "/"}, {Style: Full, Pwd: homeDir, StackCount: 0, Expected: "~"}, {Style: Full, Pwd: homeDir + abc, StackCount: 0, Expected: "~/abc"}, {Style: Full, Pwd: homeDir + abc, StackCount: 0, Expected: homeDir + abc, DisableMappedLocations: true}, {Style: Full, Pwd: abcd, StackCount: 0, Expected: abcd}, // StackCountEnabled=true and StackCount<0 {Style: Full, FolderSeparatorIcon: "|", Pwd: "/", StackCount: -1, Expected: "/"}, {Style: Full, Pwd: "/", StackCount: -1, Expected: "/"}, {Style: Full, Pwd: homeDir, StackCount: -1, Expected: "~"}, {Style: Full, Pwd: homeDir + abc, StackCount: -1, Expected: "~/abc"}, {Style: Full, Pwd: homeDir + abc, StackCount: -1, Expected: homeDir + abc, DisableMappedLocations: true}, {Style: Full, Pwd: abcd, StackCount: -1, Expected: abcd}, // StackCountEnabled=true and StackCount not set {Style: Full, FolderSeparatorIcon: "|", Pwd: "/", Expected: "/"}, {Style: Full, Pwd: "/", Expected: "/"}, {Style: Full, Pwd: homeDir, Expected: "~"}, {Style: Full, Pwd: homeDir + abc, Expected: "~/abc"}, {Style: Full, Pwd: homeDir + abc, Expected: homeDir + abc, DisableMappedLocations: true}, {Style: Full, Pwd: abcd, Expected: abcd}, } var testFullPathCustomMappedLocationsCases = []testFullPathCustomMappedLocationsCase{ {Pwd: homeDir + "/d", MappedLocations: map[string]string{"{{ .Env.HOME }}/d": "#"}, Expected: "#"}, {Pwd: abcd, MappedLocations: map[string]string{abcd: "#"}, Expected: "#"}, {Pwd: abcd, MappedLocations: map[string]string{"/a/b": "#"}, Expected: "#/c/d"}, {Pwd: abcd, MappedLocations: map[string]string{"/a/b": "/e/f"}, Expected: "/e/f/c/d"}, {Pwd: homeDir + abcd, MappedLocations: map[string]string{"~/a/b": "#"}, Expected: "#/c/d"}, {Pwd: "/a" + homeDir + "/b/c/d", MappedLocations: map[string]string{"/a~": "#"}, Expected: "/a" + homeDir + "/b/c/d"}, {Pwd: homeDir + abcd, MappedLocations: map[string]string{"/a/b": "#"}, Expected: homeDir + abcd}, } var testSplitPathCases = []testSplitPathCase{ {Case: "Root directory", Root: "/", Expected: Folders{}}, { Case: "Regular directory", Root: "/", Relative: "c/d", GOOS: runtime.DARWIN, Expected: Folders{ {Name: "c", Path: "/c"}, {Name: "d", Path: "/c/d"}, }, }, { Case: "Home directory - git folder", Root: "~", Relative: "c/d", GOOS: runtime.DARWIN, GitDir: &runtime.FileInfo{IsDir: true, ParentFolder: "/a/b/c"}, GitDirFormat: "%s", Expected: Folders{ {Name: "c", Path: "~/c", Display: true}, {Name: "d", Path: "~/c/d"}, }, }, } var testNormalizePathCases = []testNormalizePathCase{ { Case: "Linux: home prefix, backslash included", Input: "~/Bob\\Foo", HomeDir: homeDir, GOOS: runtime.LINUX, Expected: homeDir + "/Bob\\Foo", }, { Case: "macOS: home prefix, backslash included", Input: "~/Bob\\Foo", HomeDir: homeDir, GOOS: runtime.DARWIN, Expected: homeDir + "/bob\\foo", }, { Case: "Linux: absolute", Input: "/foo/~/bar", HomeDir: homeDir, GOOS: runtime.LINUX, Expected: "/foo/~/bar", }, { Case: "Linux: home prefix", Input: "~/baz", HomeDir: homeDir, GOOS: runtime.LINUX, Expected: homeDir + "/baz", }, } func TestFolderPathCustomMappedLocations(t *testing.T) { pwd := abcd env := new(mock.Environment) env.On("PathSeparator").Return("/") env.On("Home").Return(homeDir) env.On("Pwd").Return(pwd) env.On("GOOS").Return("") args := &runtime.Flags{ PSWD: pwd, } env.On("Flags").Return(args) env.On("Shell").Return(shell.GENERIC) template.Cache = new(cache.Template) template.Init(env, nil, nil) props := options.Map{ options.Style: FolderType, MappedLocations: map[string]string{ abcd: "#", }, } path := &Path{} path.Init(props, env) path.setPaths() path.setStyle() got := renderTemplateNoTrimSpace(env, "{{ .Path }}", path) assert.Equal(t, "#", got) } func TestReplaceMappedLocations(t *testing.T) { cases := []struct { Case string Pwd string Expected string MappedLocationsEnabled bool }{ {Pwd: "/c/l/k/f", Expected: "f"}, {Pwd: "/f/g/h", Expected: "/f/g/h"}, {Pwd: "/f/g/h/e", Expected: "^/e"}, {Pwd: abcd, Expected: "#"}, {Pwd: "/a/b/c/d/e", Expected: "#/e"}, {Pwd: "/a/b/c/D/e", Expected: "#/e"}, {Pwd: "/a/b/k/j/e", Expected: "e"}, {Pwd: "/a/b/k/l", Expected: "@/l"}, {Pwd: "/a/b/k/l", MappedLocationsEnabled: true, Expected: "~/l"}, } for _, tc := range cases { env := new(mock.Environment) env.On("PathSeparator").Return("/") env.On("Pwd").Return(tc.Pwd) env.On("Shell").Return(shell.FISH) env.On("GOOS").Return(runtime.DARWIN) env.On("Home").Return("/a/b/k") template.Cache = new(cache.Template) template.Init(env, nil, nil) props := options.Map{ MappedLocationsEnabled: tc.MappedLocationsEnabled, MappedLocations: map[string]string{ abcd: "#", "/f/g/h/*": "^", "/c/l/k/*": "", "~": "@", "~/j/*": "", }, } path := &Path{} path.Init(props, env) path.setPaths() assert.Equal(t, tc.Expected, path.pwd) } } func TestGetPwd(t *testing.T) { cases := []struct { Pwd string Pswd string Expected string MappedLocationsEnabled bool }{ {MappedLocationsEnabled: true, Pwd: homeDir, Expected: "~"}, {MappedLocationsEnabled: true, Pwd: homeDir + "-test", Expected: homeDir + "-test"}, {MappedLocationsEnabled: true}, {MappedLocationsEnabled: true, Pwd: "/usr", Expected: "/usr"}, {MappedLocationsEnabled: true, Pwd: homeDir + abc, Expected: "~/abc"}, {MappedLocationsEnabled: true, Pwd: abcd, Expected: "#"}, {MappedLocationsEnabled: true, Pwd: "/a/b/c/d/e/f/g", Expected: "#/e/f/g"}, {MappedLocationsEnabled: true, Pwd: "/z/y/x/w", Expected: "/z/y/x/w"}, {MappedLocationsEnabled: false}, {MappedLocationsEnabled: false, Pwd: homeDir + abc, Expected: homeDir + abc}, {MappedLocationsEnabled: false, Pwd: "/a/b/c/d/e/f/g", Expected: "#/e/f/g"}, {MappedLocationsEnabled: false, Pwd: homeDir + cdefg, Expected: homeDir + cdefg}, {MappedLocationsEnabled: true, Pwd: homeDir + cdefg, Expected: "~/c/d/e/f/g"}, {MappedLocationsEnabled: true, Pwd: "/w/d/x/w", Pswd: "/z/y/x/w", Expected: "/z/y/x/w"}, {MappedLocationsEnabled: false, Pwd: "/f/g/k/d/e/f/g", Pswd: "/a/b/c/d/e/f/g", Expected: "#/e/f/g"}, } for _, tc := range cases { env := new(mock.Environment) env.On("PathSeparator").Return("/") env.On("Home").Return(homeDir) env.On("Pwd").Return(tc.Pwd) env.On("GOOS").Return("") args := &runtime.Flags{ PSWD: tc.Pswd, } env.On("Flags").Return(args) env.On("Shell").Return(shell.PWSH) template.Cache = new(cache.Template) template.Init(env, nil, nil) props := options.Map{ MappedLocationsEnabled: tc.MappedLocationsEnabled, MappedLocations: map[string]string{ abcd: "#", }, } path := &Path{} path.Init(props, env) path.setPaths() assert.Equal(t, tc.Expected, path.pwd) } } ================================================ FILE: src/segments/path_windows_test.go ================================================ package segments import ( "errors" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/shell" ) const ( fooBarMan = "\\foo\\bar\\man" ) var testParentCases = []testParentCase{ { Case: "Windows Home folder", HomePath: homeDirWindows, Pwd: homeDirWindows, GOOS: runtime.WINDOWS, PathSeparator: `\`, }, { Case: "Windows drive root", HomePath: homeDirWindows, Pwd: "C:", GOOS: runtime.WINDOWS, PathSeparator: `\`, }, { Case: "Windows drive root with a trailing separator", HomePath: homeDirWindows, Pwd: "C:\\", GOOS: runtime.WINDOWS, PathSeparator: `\`, }, { Case: "Windows drive root + 1", Expected: "C:\\", HomePath: homeDirWindows, Pwd: "C:\\test", GOOS: runtime.WINDOWS, PathSeparator: `\`, }, { Case: "PSDrive root", HomePath: homeDirWindows, Pwd: "HKLM:", GOOS: runtime.WINDOWS, PathSeparator: `\`, }, } var testAgnosterPathStyleCases = []testAgnosterPathStyleCase{ { Style: Unique, Expected: "C > a > ab > abcd", HomePath: homeDirWindows, Pwd: "C:\\ab\\ab\\abcd", GOOS: runtime.WINDOWS, PathSeparator: `\`, FolderSeparatorIcon: " > ", }, { Style: Letter, Expected: "C: > ", HomePath: homeDirWindows, Pwd: "C:\\", GOOS: runtime.WINDOWS, PathSeparator: `\`, FolderSeparatorIcon: " > ", }, { Style: Letter, Expected: "C > s > .w > man", HomePath: homeDirWindows, Pwd: "C:\\something\\.whatever\\man", GOOS: runtime.WINDOWS, PathSeparator: `\`, FolderSeparatorIcon: " > ", }, { Style: Letter, Expected: "~ > s > man", HomePath: homeDirWindows, Pwd: homeDirWindows + "\\something\\man", GOOS: runtime.WINDOWS, PathSeparator: `\`, FolderSeparatorIcon: " > ", }, { Style: Mixed, Expected: "C: > .. > foo > .. > man", HomePath: homeDirWindows, Pwd: "C:\\Users\\foo\\foobar\\man", GOOS: runtime.WINDOWS, PathSeparator: `\`, FolderSeparatorIcon: " > ", }, { Style: Mixed, Expected: "c > .. > foo > .. > man", HomePath: homeDirWindows, Pwd: "C:\\Users\\foo\\foobar\\man", GOOS: runtime.WINDOWS, Shell: shell.BASH, Cygwin: true, Cygpath: "/c/Users/foo/foobar/man", PathSeparator: `\`, FolderSeparatorIcon: " > ", }, { Style: Mixed, Expected: "C: > .. > foo > .. > man", HomePath: homeDirWindows, Pwd: "C:\\Users\\foo\\foobar\\man", GOOS: runtime.WINDOWS, Shell: shell.BASH, CygpathError: errors.New("oh no"), PathSeparator: `\`, FolderSeparatorIcon: " > ", }, { Style: AgnosterShort, Expected: "\\\\localhost\\c$ > some", HomePath: homeDirWindows, Pwd: "\\\\localhost\\c$\\some", GOOS: runtime.WINDOWS, PathSeparator: `\`, FolderSeparatorIcon: " > ", }, { Style: AgnosterShort, Expected: "\\\\localhost\\c$", HomePath: homeDirWindows, Pwd: "\\\\localhost\\c$", GOOS: runtime.WINDOWS, PathSeparator: `\`, FolderSeparatorIcon: " > ", }, { Style: AgnosterShort, Expected: ".. > bar > man", HomePath: homeDirWindows, Pwd: homeDirWindows + fooBarMan, GOOS: runtime.WINDOWS, PathSeparator: `\`, FolderSeparatorIcon: " > ", MaxDepth: 2, HideRootLocation: true, }, { Style: AgnosterShort, Expected: "C: > ", HomePath: homeDirWindows, Pwd: "C:", GOOS: runtime.WINDOWS, PathSeparator: `\`, FolderSeparatorIcon: " > ", }, { Style: AgnosterShort, Expected: "C: > .. > bar > man", HomePath: homeDirWindows, Pwd: "C:\\usr\\foo\\bar\\man", GOOS: runtime.WINDOWS, PathSeparator: `\`, FolderSeparatorIcon: " > ", MaxDepth: 2, }, { Style: AgnosterShort, Expected: "C: > .. > foo > bar > man", HomePath: homeDirWindows, Pwd: "C:\\usr\\foo\\bar\\man", GOOS: runtime.WINDOWS, PathSeparator: `\`, FolderSeparatorIcon: " > ", MaxDepth: 3, }, { Style: AgnosterShort, Expected: "~ > .. > bar > man", HomePath: homeDirWindows, Pwd: homeDirWindows + fooBarMan, GOOS: runtime.WINDOWS, PathSeparator: `\`, FolderSeparatorIcon: " > ", MaxDepth: 2, }, { Style: AgnosterShort, Expected: "~ > foo > bar > man", HomePath: homeDirWindows, Pwd: homeDirWindows + fooBarMan, GOOS: runtime.WINDOWS, PathSeparator: `\`, FolderSeparatorIcon: " > ", MaxDepth: 3, }, { Style: AgnosterShort, Expected: "~", HomePath: homeDirWindows, Pwd: homeDirWindows, GOOS: runtime.WINDOWS, PathSeparator: `\`, FolderSeparatorIcon: " > ", MaxDepth: 1, HideRootLocation: true, }, { Style: AgnosterShort, Expected: ".. > foo", HomePath: homeDirWindows, Pwd: homeDirWindows + "\\foo", GOOS: runtime.WINDOWS, PathSeparator: `\`, FolderSeparatorIcon: " > ", MaxDepth: 1, HideRootLocation: true, }, { Style: AgnosterShort, Expected: "~ > foo", HomePath: homeDirWindows, Pwd: homeDirWindows + "\\foo", GOOS: runtime.WINDOWS, PathSeparator: `\`, FolderSeparatorIcon: " > ", MaxDepth: 2, HideRootLocation: true, }, } var testAgnosterPathCases = []testAgnosterPathCase{ { Case: "Windows registry drive case sensitive", Expected: "\uf013 > f > magnetic:TOAST", Home: homeDirWindows, PWD: "HKLM:\\SOFTWARE\\magnetic:TOAST\\", GOOS: runtime.WINDOWS, PathSeparator: `\`, }, { Case: "Windows outside home", Expected: "C: > f > f > location", Home: homeDirWindows, PWD: "C:\\Program Files\\Go\\location", GOOS: runtime.WINDOWS, PathSeparator: `\`, }, { Case: "Windows outside home", Expected: "~ > f > f > location", Home: homeDirWindows, PWD: homeDirWindows + "\\Documents\\Bill\\location", GOOS: runtime.WINDOWS, PathSeparator: `\`, }, { Case: "Windows inside home zero levels", Expected: "C: > location", Home: homeDirWindows, PWD: "C:\\location", GOOS: runtime.WINDOWS, PathSeparator: `\`, }, { Case: "Windows inside home one level", Expected: "C: > f > location", Home: homeDirWindows, PWD: "C:\\Program Files\\location", GOOS: runtime.WINDOWS, PathSeparator: `\`, }, { Case: "Windows lower case drive letter", Expected: "C: > Windows", Home: homeDirWindows, PWD: "C:\\Windows\\", GOOS: runtime.WINDOWS, PathSeparator: `\`, }, { Case: "Windows lower case drive letter (other)", Expected: "P: > Other", Home: homeDirWindows, PWD: "P:\\Other\\", GOOS: runtime.WINDOWS, PathSeparator: `\`, }, { Case: "Windows lower word drive", Expected: "some: > some", Home: homeDirWindows, PWD: "some:\\some\\", GOOS: runtime.WINDOWS, PathSeparator: `\`, }, { Case: "Windows lower word drive (ending with c)", Expected: "src: > source", Home: homeDirWindows, PWD: "src:\\source\\", GOOS: runtime.WINDOWS, PathSeparator: `\`, }, { Case: "Windows lower word drive (arbitrary cases)", Expected: "sRc: > source", Home: homeDirWindows, PWD: "sRc:\\source\\", GOOS: runtime.WINDOWS, PathSeparator: `\`, }, { Case: "Windows registry drive", Expected: "\uf013 > f > magnetic:test", Home: homeDirWindows, PWD: "HKLM:\\SOFTWARE\\magnetic:test\\", GOOS: runtime.WINDOWS, PathSeparator: `\`, }, } var testAgnosterLeftPathCases = []testAgnosterLeftPathCase{ { Case: "Windows inside home", Expected: "~ > Documents > f > f", Home: homeDirWindows, PWD: homeDirWindows + "\\Documents\\Bill\\location", GOOS: runtime.WINDOWS, PathSeparator: `\`, }, { Case: "Windows outside home", Expected: "C: > Program Files > f > f", Home: homeDirWindows, PWD: "C:\\Program Files\\Go\\location", GOOS: runtime.WINDOWS, PathSeparator: `\`, }, { Case: "Windows inside home zero levels", Expected: "C: > location", Home: homeDirWindows, PWD: "C:\\location", GOOS: runtime.WINDOWS, PathSeparator: `\`, }, { Case: "Windows inside home one level", Expected: "C: > Program Files > f", Home: homeDirWindows, PWD: "C:\\Program Files\\location", GOOS: runtime.WINDOWS, PathSeparator: `\`, }, { Case: "Windows lower case drive letter", Expected: "C: > Windows", Home: homeDirWindows, PWD: "C:\\Windows\\", GOOS: runtime.WINDOWS, PathSeparator: `\`, }, { Case: "Windows lower case drive letter (other)", Expected: "P: > Other", Home: homeDirWindows, PWD: "P:\\Other\\", GOOS: runtime.WINDOWS, PathSeparator: `\`, }, { Case: "Windows lower word drive", Expected: "some: > some", Home: homeDirWindows, PWD: "some:\\some\\", GOOS: runtime.WINDOWS, PathSeparator: `\`, }, { Case: "Windows lower word drive (ending with c)", Expected: "src: > source", Home: homeDirWindows, PWD: "src:\\source\\", GOOS: runtime.WINDOWS, PathSeparator: `\`, }, { Case: "Windows lower word drive (arbitrary cases)", Expected: "sRc: > source", Home: homeDirWindows, PWD: "sRc:\\source\\", GOOS: runtime.WINDOWS, PathSeparator: `\`, }, { Case: "Windows registry drive", Expected: "\uf013 > SOFTWARE > f", Home: homeDirWindows, PWD: "HKLM:\\SOFTWARE\\magnetic:test\\", GOOS: runtime.WINDOWS, PathSeparator: `\`, }, { Case: "Windows registry drive case sensitive", Expected: "\uf013 > SOFTWARE > f", Home: homeDirWindows, PWD: "HKLM:\\SOFTWARE\\magnetic:TOAST\\", GOOS: runtime.WINDOWS, PathSeparator: `\`, }, } var testFullAndFolderPathCases = []testFullAndFolderPathCase{ {Style: FolderType, FolderSeparatorIcon: `\`, Pwd: "C:\\", Expected: "C:\\", PathSeparator: `\`, GOOS: runtime.WINDOWS}, {Style: FolderType, FolderSeparatorIcon: `\`, Pwd: "\\\\localhost\\d$", Expected: "\\\\localhost\\d$", PathSeparator: `\`, GOOS: runtime.WINDOWS}, {Style: FolderType, FolderSeparatorIcon: `\`, Pwd: homeDirWindows, Expected: "~", PathSeparator: `\`, GOOS: runtime.WINDOWS}, {Style: Full, FolderSeparatorIcon: `\`, Pwd: homeDirWindows, Expected: "~", PathSeparator: `\`, GOOS: runtime.WINDOWS}, {Style: Full, FolderSeparatorIcon: `\`, Pwd: homeDirWindows + "\\abc", Expected: "~\\abc", PathSeparator: `\`, GOOS: runtime.WINDOWS}, {Style: Full, FolderSeparatorIcon: `\`, Pwd: "C:\\Users\\posh", Expected: "C:\\Users\\posh", PathSeparator: `\`, GOOS: runtime.WINDOWS}, } var testFullPathCustomMappedLocationsCases = []testFullPathCustomMappedLocationsCase{ { Pwd: `\a\b\c\d`, MappedLocations: map[string]string{`\a\b`: "#"}, GOOS: runtime.WINDOWS, PathSeparator: `\`, Expected: `#\c\d`, }, { Pwd: `\a\b\1234\d\e`, MappedLocations: map[string]string{`re:(/a/b/[0-9]+/d).*`: "#"}, GOOS: runtime.WINDOWS, PathSeparator: `\`, Expected: `#\e`, }, { Pwd: `\a\b\1234\f\e`, MappedLocations: map[string]string{`re:(/a/b/[0-9]+/d).*`: "#"}, GOOS: runtime.WINDOWS, PathSeparator: `\`, Expected: `\a\b\1234\f\e`, }, { Pwd: `C:\Users\taylo\Documents\github\project`, MappedLocations: map[string]string{`re:(.*Users/taylo/Documents/GitHub).*`: "github"}, GOOS: runtime.WINDOWS, PathSeparator: `\`, Expected: `github\project`, }, } var testSplitPathCases = []testSplitPathCase{ { Case: "Home directory - git folder on Windows", Root: "C:", Relative: "a/b/c/d", GOOS: runtime.WINDOWS, GitDir: &runtime.FileInfo{IsDir: true, ParentFolder: "C:/a/b/c"}, GitDirFormat: "%s", Expected: Folders{ {Name: "a", Path: "C:/a"}, {Name: "b", Path: "C:/a/b"}, {Name: "c", Path: "C:/a/b/c", Display: true}, {Name: "d", Path: "C:/a/b/c/d"}, }, }, } var testNormalizePathCases = []testNormalizePathCase{ { Case: "Windows: absolute w/o drive letter, forward slash included", Input: "/foo/~/bar", HomeDir: homeDirWindows, GOOS: runtime.WINDOWS, PathSeparator: `\`, Expected: "\\foo\\~\\bar", }, { Case: "Windows: absolute", Input: homeDirWindows + "\\Foo", HomeDir: homeDirWindows, GOOS: runtime.WINDOWS, PathSeparator: `\`, Expected: "c:\\users\\someone\\foo", }, { Case: "Windows: home prefix", Input: "~\\Bob\\Foo", HomeDir: homeDirWindows, GOOS: runtime.WINDOWS, PathSeparator: `\`, Expected: "c:\\users\\someone\\bob\\foo", }, { Case: "Windows: home prefix", Input: "~/baz", HomeDir: homeDirWindows, GOOS: runtime.WINDOWS, PathSeparator: `\`, Expected: "c:\\users\\someone\\baz", }, { Case: "Windows: UNC root w/ prefix", Input: `\\.\UNC\localhost\c$`, HomeDir: homeDirWindows, GOOS: runtime.WINDOWS, PathSeparator: `\`, Expected: "\\\\localhost\\c$", }, { Case: "Windows: UNC root w/ prefix, forward slash included", Input: "//./UNC/localhost/c$", HomeDir: homeDirWindows, GOOS: runtime.WINDOWS, PathSeparator: `\`, Expected: "\\\\localhost\\c$", }, { Case: "Windows: UNC root", Input: `\\localhost\c$\`, HomeDir: homeDirWindows, GOOS: runtime.WINDOWS, PathSeparator: `\`, Expected: "\\\\localhost\\c$", }, { Case: "Windows: UNC root, forward slash included", Input: "//localhost/c$", HomeDir: homeDirWindows, GOOS: runtime.WINDOWS, PathSeparator: `\`, Expected: "\\\\localhost\\c$", }, { Case: "Windows: UNC", Input: `\\localhost\c$\some`, HomeDir: homeDirWindows, GOOS: runtime.WINDOWS, PathSeparator: `\`, Expected: "\\\\localhost\\c$\\some", }, { Case: "Windows: UNC, forward slash included", Input: "//localhost/c$/some", HomeDir: homeDirWindows, GOOS: runtime.WINDOWS, PathSeparator: `\`, Expected: "\\\\localhost\\c$\\some", }, { Case: "Windows: display Cygwin path", Input: fooBarMan, HomeDir: homeDirWindows, GOOS: runtime.WINDOWS, PathSeparator: `\`, Expected: "/foo/bar/man", Cygwin: true, }, } ================================================ FILE: src/segments/perl.go ================================================ package segments type Perl struct { Language } func (p *Perl) Template() string { return languageTemplate } func (p *Perl) Enabled() bool { perlRegex := `This is perl.*v(?P(?P[0-9]+)(?:\.(?P[0-9]+))(?:\.(?P[0-9]+))?).* built for .+` p.extensions = []string{ ".perl-version", "*.pl", "*.pm", "*.t", } p.tooling = map[string]*cmd{ "perl": { executable: "perl", args: []string{"-version"}, regex: perlRegex, }, } p.defaultTooling = []string{"perl"} return p.Language.Enabled() } ================================================ FILE: src/segments/perl_test.go ================================================ package segments import ( "fmt" "testing" "github.com/stretchr/testify/assert" ) func TestPerl(t *testing.T) { cases := []struct { Case string ExpectedString string Version string PerlHomeVersion string PerlHomeEnabled bool }{ { Case: "v5.12+", ExpectedString: "5.32.1", Version: "This is perl 5, version 32, subversion 1 (v5.32.1) built for MSWin32-x64-multi-thread", }, { Case: "v5.6 - v5.10", ExpectedString: "5.6.1", Version: "This is perl, v5.6.1 built for MSWin32-x86-multi-thread", }, } for _, tc := range cases { params := &mockedLanguageParams{ cmd: "perl", versionParam: "-version", versionOutput: tc.Version, extension: ".perl-version", } env, props := getMockedLanguageEnv(params) p := &Perl{} p.Init(props, env) assert.True(t, p.Enabled(), fmt.Sprintf("Failed in case: %s", tc.Case)) assert.Equal(t, tc.ExpectedString, renderTemplate(env, p.Template(), p), fmt.Sprintf("Failed in case: %s", tc.Case)) } } ================================================ FILE: src/segments/php.go ================================================ package segments type Php struct { Language } func (p *Php) Template() string { return languageTemplate } func (p *Php) Enabled() bool { p.extensions = []string{"*.php", "composer.json", "composer.lock", ".php-version", "blade.php"} p.tooling = map[string]*cmd{ "php": { executable: "php", args: []string{"--version"}, regex: `(?:PHP (?P((?P[0-9]+).(?P[0-9]+).(?P[0-9]+))))`, }, } p.defaultTooling = []string{"php"} p.versionURLTemplate = "https://www.php.net/ChangeLog-{{ .Major }}.php#PHP_{{ .Major }}_{{ .Minor }}" return p.Language.Enabled() } ================================================ FILE: src/segments/php_test.go ================================================ package segments import ( "fmt" "testing" "github.com/stretchr/testify/assert" ) func TestPhp(t *testing.T) { cases := []struct { Case string ExpectedString string Version string }{ {Case: "PHP 6.1.0", ExpectedString: "6.1.0", Version: "PHP 6.1.0(cli) (built: Jul 2 2021 03:59:48) ( NTS )"}, {Case: "php 7.4.21", ExpectedString: "7.4.21", Version: "PHP 7.4.21 (cli) (built: Jul 2 2021 03:59:48) ( NTS )"}, } for _, tc := range cases { params := &mockedLanguageParams{ cmd: "php", versionParam: "--version", versionOutput: tc.Version, extension: "*.php", } env, props := getMockedLanguageEnv(params) j := &Php{} j.Init(props, env) assert.True(t, j.Enabled(), fmt.Sprintf("Failed in case: %s", tc.Case)) assert.Equal(t, tc.ExpectedString, renderTemplate(env, j.Template(), j), fmt.Sprintf("Failed in case: %s", tc.Case)) } } ================================================ FILE: src/segments/plastic.go ================================================ package segments import ( "fmt" "strconv" "strings" "github.com/jandedobbeleer/oh-my-posh/src/regex" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/segments/options" ) type PlasticStatus struct { ScmStatus } func (s *PlasticStatus) add(code string) { switch code { case "LD": s.Deleted++ case "AD", "PR": s.Added++ case "LM": s.Moved++ case "CH", "CO": s.Modified++ } } type Plastic struct { Status *PlasticStatus Selector string plasticWorkspaceFolder string Scm Behind bool MergePending bool } func (p *Plastic) Init(props options.Provider, env runtime.Environment) { p.options = props p.env = env } func (p *Plastic) Template() string { return " {{ .Selector }} " } func (p *Plastic) Enabled() bool { if !p.env.HasCommand("cm") { return false } wkdir, err := p.env.HasParentFilePath(".plastic", false) if err != nil { return false } if !wkdir.IsDir { return false } p.plasticWorkspaceFolder = wkdir.ParentFolder displayStatus := p.options.Bool(FetchStatus, false) p.setSelector() if displayStatus { p.setPlasticStatus() } return true } func (p *Plastic) CacheKey() (string, bool) { dir, err := p.env.HasParentFilePath(".plastic", true) if err != nil { return "", false } return dir.Path, true } func (p *Plastic) setPlasticStatus() { output := p.getCmCommandOutput("status", "--all", "--machinereadable") splittedOutput := strings.Split(output, "\n") // compare to head currentChangeset := p.parseStatusChangeset(splittedOutput[0]) headChangeset := p.getHeadChangeset() p.Behind = headChangeset > currentChangeset statusFormats := p.options.KeyValueMap(StatusFormats, map[string]string{}) p.Status = &PlasticStatus{ScmStatus: ScmStatus{Formats: statusFormats}} // parse file state p.MergePending = false p.parseFilesStatus(splittedOutput) } func (p *Plastic) parseFilesStatus(output []string) { if len(output) <= 1 { return } for _, line := range output[1:] { if len(line) < 3 { continue } if strings.Contains(line, "NO_MERGES") { p.Status.Unmerged++ continue } p.MergePending = p.MergePending || regex.MatchString(`(?i)\smerge\s+from\s+[0-9]+\s*$`, line) code := line[:2] p.Status.add(code) } } func (p *Plastic) parseStringPattern(output, pattern, name string) string { match := regex.FindNamedRegexMatch(pattern, output) if sValue, ok := match[name]; ok { return sValue } return "" } func (p *Plastic) parseIntPattern(output, pattern, name string, defValue int) int { sValue := p.parseStringPattern(output, pattern, name) if len(sValue) > 0 { iValue, _ := strconv.Atoi(sValue) return iValue } return defValue } func (p *Plastic) parseStatusChangeset(status string) int { return p.parseIntPattern(status, `STATUS\s+(?P[0-9]+?)\s`, "cs", 0) } func (p *Plastic) getHeadChangeset() int { output := p.getCmCommandOutput("status", "--head", "--machinereadable") return p.parseIntPattern(output, `\bcs:(?P[0-9]+?)\s`, "cs", 0) } func (p *Plastic) setSelector() { var ref string selector := p.fileContent(p.plasticWorkspaceFolder+"/.plastic/", "plastic.selector") // changeset ref = p.parseChangesetSelector(selector) if len(ref) > 0 { p.Selector = fmt.Sprintf("%s%s", p.options.String(CommitIcon, "\uF417"), ref) return } // fallback to label ref = p.parseLabelSelector(selector) if len(ref) > 0 { p.Selector = fmt.Sprintf("%s%s", p.options.String(TagIcon, "\uF412"), ref) return } // fallback to branch/smartbranch ref = p.parseBranchSelector(selector) if len(ref) > 0 { ref = p.formatBranch(ref) } p.Selector = fmt.Sprintf("%s%s", p.options.String(BranchIcon, "\uE0A0"), ref) } func (p *Plastic) parseChangesetSelector(selector string) string { return p.parseStringPattern(selector, `\bchangeset "(?P[0-9]+?)"`, "cs") } func (p *Plastic) parseLabelSelector(selector string) string { return p.parseStringPattern(selector, `label "(?P