| author | |
| committer | |
| log | e204f5a5d34c753399531e772fbb11420caeea09 |
| tree | 03161095d89413bb74b559dced06c53087c51385 |
| parent | 3cde99b259249a02e50079f326a00b5ba38e7235 |
| signature | Signed by PGP key B5690EEEBB952194 |
2 files changed, 130 insertions(+), 0 deletions(-)
apps/desktop/src/shell/DesktopShellEnvironment.test.ts+102| ... | @@ -152,6 +152,108 @@ describe("DesktopShellEnvironment", () => { | ... | @@ -152,6 +152,108 @@ describe("DesktopShellEnvironment", () => { |
| 152 | }), | 152 | }), |
| 153 | ); | 153 | ); |
| 154 | 154 | ||
| 155 | it.effect("hydrates the locale from the login shell on macOS", () => | ||
| 156 | Effect.gen(function* () { | ||
| 157 | const env: NodeJS.ProcessEnv = { | ||
| 158 | SHELL: "/bin/zsh", | ||
| 159 | PATH: "/usr/bin", | ||
| 160 | }; | ||
| 161 | |||
| 162 | yield* runShellEnvironment({ | ||
| 163 | env, | ||
| 164 | platform: "darwin", | ||
| 165 | handler: () => | ||
| 166 | envOutput({ | ||
| 167 | PATH: "/opt/homebrew/bin:/usr/bin", | ||
| 168 | LANG: "de_DE.UTF-8", | ||
| 169 | }), | ||
| 170 | }); | ||
| 171 | |||
| 172 | assert.equal(env.LANG, "de_DE.UTF-8"); | ||
| 173 | }), | ||
| 174 | ); | ||
| 175 | |||
| 176 | it.effect("preserves an inherited locale over the login shell on macOS", () => | ||
| 177 | Effect.gen(function* () { | ||
| 178 | const env: NodeJS.ProcessEnv = { | ||
| 179 | SHELL: "/bin/zsh", | ||
| 180 | PATH: "/usr/bin", | ||
| 181 | LANG: "en_US.UTF-8", | ||
| 182 | }; | ||
| 183 | |||
| 184 | yield* runShellEnvironment({ | ||
| 185 | env, | ||
| 186 | platform: "darwin", | ||
| 187 | handler: () => | ||
| 188 | envOutput({ | ||
| 189 | PATH: "/opt/homebrew/bin:/usr/bin", | ||
| 190 | LANG: "de_DE.UTF-8", | ||
| 191 | }), | ||
| 192 | }); | ||
| 193 | |||
| 194 | assert.equal(env.LANG, "en_US.UTF-8"); | ||
| 195 | }), | ||
| 196 | ); | ||
| 197 | |||
| 198 | it.effect("does not mix login-shell locale categories into an inherited locale", () => | ||
| 199 | Effect.gen(function* () { | ||
| 200 | const env: NodeJS.ProcessEnv = { | ||
| 201 | SHELL: "/bin/zsh", | ||
| 202 | PATH: "/usr/bin", | ||
| 203 | LANG: "en_US.UTF-8", | ||
| 204 | }; | ||
| 205 | |||
| 206 | yield* runShellEnvironment({ | ||
| 207 | env, | ||
| 208 | platform: "darwin", | ||
| 209 | handler: () => | ||
| 210 | envOutput({ | ||
| 211 | PATH: "/opt/homebrew/bin:/usr/bin", | ||
| 212 | LC_ALL: "de_DE.UTF-8", | ||
| 213 | }), | ||
| 214 | }); | ||
| 215 | |||
| 216 | assert.equal(env.LANG, "en_US.UTF-8"); | ||
| 217 | assert.equal(env.LC_ALL, undefined); | ||
| 218 | }), | ||
| 219 | ); | ||
| 220 | |||
| 221 | it.effect("falls back to a UTF-8 LC_CTYPE when no locale is available on macOS", () => | ||
| 222 | Effect.gen(function* () { | ||
| 223 | const env: NodeJS.ProcessEnv = { | ||
| 224 | SHELL: "/bin/zsh", | ||
| 225 | PATH: "/usr/bin", | ||
| 226 | }; | ||
| 227 | |||
| 228 | yield* runShellEnvironment({ | ||
| 229 | env, | ||
| 230 | platform: "darwin", | ||
| 231 | handler: () => envOutput({ PATH: "/opt/homebrew/bin:/usr/bin" }), | ||
| 232 | }); | ||
| 233 | |||
| 234 | assert.equal(env.LANG, undefined); | ||
| 235 | assert.equal(env.LC_ALL, undefined); | ||
| 236 | assert.equal(env.LC_CTYPE, "en_US.UTF-8"); | ||
| 237 | }), | ||
| 238 | ); | ||
| 239 | |||
| 240 | it.effect("does not apply the locale fallback on linux", () => | ||
| 241 | Effect.gen(function* () { | ||
| 242 | const env: NodeJS.ProcessEnv = { | ||
| 243 | SHELL: "/bin/zsh", | ||
| 244 | PATH: "/usr/bin", | ||
| 245 | }; | ||
| 246 | |||
| 247 | yield* runShellEnvironment({ | ||
| 248 | env, | ||
| 249 | platform: "linux", | ||
| 250 | handler: () => envOutput({ PATH: "/home/linuxbrew/.linuxbrew/bin:/usr/bin" }), | ||
| 251 | }); | ||
| 252 | |||
| 253 | assert.equal(env.LANG, undefined); | ||
| 254 | }), | ||
| 255 | ); | ||
| 256 | |||
| 155 | it.effect("hydrates PATH and missing SSH_AUTH_SOCK from the login shell on linux", () => | 257 | it.effect("hydrates PATH and missing SSH_AUTH_SOCK from the login shell on linux", () => |
| 156 | Effect.gen(function* () { | 258 | Effect.gen(function* () { |
| 157 | const env: NodeJS.ProcessEnv = { | 259 | const env: NodeJS.ProcessEnv = { |
apps/desktop/src/shell/DesktopShellEnvironment.ts+28| ... | @@ -71,6 +71,9 @@ const LOGIN_SHELL_ENV_NAMES = [ | ... | @@ -71,6 +71,9 @@ const LOGIN_SHELL_ENV_NAMES = [ |
| 71 | "PATH", | 71 | "PATH", |
| 72 | "DBUS_SESSION_BUS_ADDRESS", | 72 | "DBUS_SESSION_BUS_ADDRESS", |
| 73 | "DISPLAY", | 73 | "DISPLAY", |
| 74 | "LANG", | ||
| 75 | "LC_ALL", | ||
| 76 | "LC_CTYPE", | ||
| 74 | "SSH_AUTH_SOCK", | 77 | "SSH_AUTH_SOCK", |
| 75 | "HOMEBREW_PREFIX", | 78 | "HOMEBREW_PREFIX", |
| 76 | "HOMEBREW_CELLAR", | 79 | "HOMEBREW_CELLAR", |
| ... | @@ -84,6 +87,8 @@ const LOGIN_SHELL_ENV_NAMES = [ | ... | @@ -84,6 +87,8 @@ const LOGIN_SHELL_ENV_NAMES = [ |
| 84 | "WAYLAND_DISPLAY", | 87 | "WAYLAND_DISPLAY", |
| 85 | ] as const; | 88 | ] as const; |
| 86 | const WINDOWS_PROFILE_ENV_NAMES = ["PATH", "FNM_DIR", "FNM_MULTISHELL_PATH"] as const; | 89 | const WINDOWS_PROFILE_ENV_NAMES = ["PATH", "FNM_DIR", "FNM_MULTISHELL_PATH"] as const; |
| 90 | const LOCALE_ENV_NAMES = ["LANG", "LC_ALL", "LC_CTYPE"] as const; | ||
| 91 | const FALLBACK_LC_CTYPE = "en_US.UTF-8"; | ||
| 87 | const WINDOWS_SHELL_CANDIDATES = ["pwsh.exe", "powershell.exe"] as const; | 92 | const WINDOWS_SHELL_CANDIDATES = ["pwsh.exe", "powershell.exe"] as const; |
| 88 | const LOGIN_SHELL_TIMEOUT = Duration.seconds(5); | 93 | const LOGIN_SHELL_TIMEOUT = Duration.seconds(5); |
| 89 | const LAUNCHCTL_TIMEOUT = Duration.seconds(2); | 94 | const LAUNCHCTL_TIMEOUT = Duration.seconds(2); |
| ... | @@ -472,6 +477,29 @@ const installPosixEnvironment = Effect.fn("desktop.shellEnvironment.installPosix | ... | @@ -472,6 +477,29 @@ const installPosixEnvironment = Effect.fn("desktop.shellEnvironment.installPosix |
| 472 | } | 477 | } |
| 473 | } | 478 | } |
| 474 | 479 | ||
| 480 | // Locale variables form one precedence group: LC_ALL can override an inherited | ||
| 481 | // LANG or LC_CTYPE, so only hydrate the group when the process has none of them. | ||
| 482 | if ( | ||
| 483 | config.platform === "darwin" && | ||
| 484 | LOCALE_ENV_NAMES.every((name) => Option.isNone(trimNonEmpty(config.env[name]))) | ||
| 485 | ) { | ||
| 486 | for (const name of LOCALE_ENV_NAMES) { | ||
| 487 | const value = trimNonEmpty(shellEnvironment[name]); | ||
| 488 | if (Option.isSome(value)) { | ||
| 489 | config.env[name] = value.value; | ||
| 490 | } | ||
| 491 | } | ||
| 492 | |||
| 493 | // GUI launches inherit no locale from launchd, so spawned agents land in the C | ||
| 494 | // locale and pbcopy decodes their UTF-8 output as MacRoman. Older supported | ||
| 495 | // macOS releases do not provide C.UTF-8, so set only LC_CTYPE to a UTF-8 locale | ||
| 496 | // available on those releases. Leaving LANG unset keeps C-stable collation and | ||
| 497 | // formatting, so output parsing is unaffected. | ||
| 498 | if (LOCALE_ENV_NAMES.every((name) => Option.isNone(trimNonEmpty(config.env[name])))) { | ||
| 499 | config.env.LC_CTYPE = FALLBACK_LC_CTYPE; | ||
| 500 | } | ||
| 501 | } | ||
| 502 | |||
| 475 | if ( | 503 | if ( |
| 476 | config.platform === "linux" && | 504 | config.platform === "linux" && |
| 477 | Option.isNone(trimNonEmpty(config.env.DBUS_SESSION_BUS_ADDRESS)) | 505 | Option.isNone(trimNonEmpty(config.env.DBUS_SESSION_BUS_ADDRESS)) |