mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
core: add env
config option
Fixes #5257 Specify environment variables to pass to commands launched in a terminal surface. The format is `env=KEY=VALUE`. `env = foo=bar` `env = bar=baz` Setting `env` to an empty string will reset the entire map to default (empty). `env =` Setting a key to an empty string will remove that particular key and corresponding value from the map. `env = foo=bar` `env = foo=` will result in `foo` not being passed to the launched commands. Setting a key multiple times will overwrite previous entries. `env = foo=bar` `env = foo=baz` will result in `foo=baz` being passed to the launched commands. These environment variables _will not_ be passed to commands run by Ghostty for other purposes, like `open` or `xdg-open` used to open URLs in your browser.
This commit is contained in:

committed by
Mitchell Hashimoto

parent
b975f1e860
commit
c7971b562e
@ -531,6 +531,7 @@ pub fn init(
|
||||
var io_exec = try termio.Exec.init(alloc, .{
|
||||
.command = command,
|
||||
.env = env,
|
||||
.env_override = config.env,
|
||||
.shell_integration = config.@"shell-integration",
|
||||
.shell_integration_features = config.@"shell-integration-features",
|
||||
.working_directory = config.@"working-directory",
|
||||
|
@ -27,6 +27,7 @@ pub const OptionAsAlt = Config.OptionAsAlt;
|
||||
pub const RepeatableCodepointMap = Config.RepeatableCodepointMap;
|
||||
pub const RepeatableFontVariation = Config.RepeatableFontVariation;
|
||||
pub const RepeatableString = Config.RepeatableString;
|
||||
pub const RepeatableStringMap = @import("config/RepeatableStringMap.zig");
|
||||
pub const RepeatablePath = Config.RepeatablePath;
|
||||
pub const ShellIntegrationFeatures = Config.ShellIntegrationFeatures;
|
||||
pub const WindowPaddingColor = Config.WindowPaddingColor;
|
||||
|
@ -34,6 +34,7 @@ const KeyValue = @import("key.zig").Value;
|
||||
const ErrorList = @import("ErrorList.zig");
|
||||
const MetricModifier = fontpkg.Metrics.Modifier;
|
||||
const help_strings = @import("help_strings");
|
||||
const RepeatableStringMap = @import("RepeatableStringMap.zig");
|
||||
|
||||
const log = std.log.scoped(.config);
|
||||
|
||||
@ -735,6 +736,42 @@ command: ?[]const u8 = null,
|
||||
///
|
||||
@"initial-command": ?[]const u8 = null,
|
||||
|
||||
/// Extra environment variables to pass to commands launched in a terminal
|
||||
/// surface. The format is `env=KEY=VALUE`.
|
||||
///
|
||||
/// `env = foo=bar`
|
||||
/// `env = bar=baz`
|
||||
///
|
||||
/// Setting `env` to an empty string will reset the entire map to default
|
||||
/// (empty).
|
||||
///
|
||||
/// `env =`
|
||||
///
|
||||
/// Setting a key to an empty string will remove that particular key and
|
||||
/// corresponding value from the map.
|
||||
///
|
||||
/// `env = foo=bar`
|
||||
/// `env = foo=`
|
||||
///
|
||||
/// will result in `foo` not being passed to the launched commands.
|
||||
///
|
||||
/// Setting a key multiple times will overwrite previous entries.
|
||||
///
|
||||
/// `env = foo=bar`
|
||||
/// `env = foo=baz`
|
||||
///
|
||||
/// will result in `foo=baz` being passed to the launched commands.
|
||||
///
|
||||
/// These environment variables will override any existing environment
|
||||
/// variables set by Ghostty. For example, if you set `GHOSTTY_RESOURCES_DIR`
|
||||
/// then the value you set here will override the value Ghostty typically
|
||||
/// automatically injects.
|
||||
///
|
||||
/// These environment variables _will not_ be passed to commands run by Ghostty
|
||||
/// for other purposes, like `open` or `xdg-open` used to open URLs in your
|
||||
/// browser.
|
||||
env: RepeatableStringMap = .{},
|
||||
|
||||
/// If true, keep the terminal open after the command exits. Normally, the
|
||||
/// terminal window closes when the running command (such as a shell) exits.
|
||||
/// With this true, the terminal window will stay open until any keypress is
|
||||
|
198
src/config/RepeatableStringMap.zig
Normal file
198
src/config/RepeatableStringMap.zig
Normal file
@ -0,0 +1,198 @@
|
||||
/// RepeatableStringMap is a key/value that can be repeated to accumulate a
|
||||
/// string map. This isn't called "StringMap" because I find that sometimes
|
||||
/// leads to confusion that it _accepts_ a map such as JSON dict.
|
||||
const RepeatableStringMap = @This();
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const formatterpkg = @import("formatter.zig");
|
||||
|
||||
const Map = std.ArrayHashMapUnmanaged(
|
||||
[:0]const u8,
|
||||
[:0]const u8,
|
||||
std.array_hash_map.StringContext,
|
||||
true,
|
||||
);
|
||||
|
||||
// Allocator for the list is the arena for the parent config.
|
||||
map: Map = .{},
|
||||
|
||||
pub fn parseCLI(
|
||||
self: *RepeatableStringMap,
|
||||
alloc: Allocator,
|
||||
input: ?[]const u8,
|
||||
) !void {
|
||||
const value = input orelse return error.ValueRequired;
|
||||
|
||||
// Empty value resets the list. We don't need to free our values because
|
||||
// the allocator used is always an arena.
|
||||
if (value.len == 0) {
|
||||
self.map.clearRetainingCapacity();
|
||||
return;
|
||||
}
|
||||
|
||||
const index = std.mem.indexOfScalar(
|
||||
u8,
|
||||
value,
|
||||
'=',
|
||||
) orelse return error.ValueRequired;
|
||||
|
||||
const key = std.mem.trim(u8, value[0..index], &std.ascii.whitespace);
|
||||
const val = std.mem.trim(u8, value[index + 1 ..], &std.ascii.whitespace);
|
||||
|
||||
const key_copy = try alloc.dupeZ(u8, key);
|
||||
errdefer alloc.free(key_copy);
|
||||
|
||||
// Empty value removes the key from the map.
|
||||
if (val.len == 0) {
|
||||
_ = self.map.orderedRemove(key_copy);
|
||||
alloc.free(key_copy);
|
||||
return;
|
||||
}
|
||||
|
||||
const val_copy = try alloc.dupeZ(u8, val);
|
||||
errdefer alloc.free(val_copy);
|
||||
|
||||
try self.map.put(alloc, key_copy, val_copy);
|
||||
}
|
||||
|
||||
/// Deep copy of the struct. Required by Config.
|
||||
pub fn clone(
|
||||
self: *const RepeatableStringMap,
|
||||
alloc: Allocator,
|
||||
) Allocator.Error!RepeatableStringMap {
|
||||
var map: Map = .{};
|
||||
try map.ensureTotalCapacity(alloc, self.map.count());
|
||||
|
||||
errdefer {
|
||||
var it = map.iterator();
|
||||
while (it.next()) |entry| {
|
||||
alloc.free(entry.key_ptr.*);
|
||||
alloc.free(entry.value_ptr.*);
|
||||
}
|
||||
map.deinit(alloc);
|
||||
}
|
||||
|
||||
var it = self.map.iterator();
|
||||
while (it.next()) |entry| {
|
||||
const key = try alloc.dupeZ(u8, entry.key_ptr.*);
|
||||
const value = try alloc.dupeZ(u8, entry.value_ptr.*);
|
||||
map.putAssumeCapacity(key, value);
|
||||
}
|
||||
|
||||
return .{ .map = map };
|
||||
}
|
||||
|
||||
/// The number of items in the map
|
||||
pub fn count(self: RepeatableStringMap) usize {
|
||||
return self.map.count();
|
||||
}
|
||||
|
||||
/// Iterator over the entries in the map.
|
||||
pub fn iterator(self: RepeatableStringMap) Map.Iterator {
|
||||
return self.map.iterator();
|
||||
}
|
||||
|
||||
/// Compare if two of our value are requal. Required by Config.
|
||||
pub fn equal(self: RepeatableStringMap, other: RepeatableStringMap) bool {
|
||||
if (self.map.count() != other.map.count()) return false;
|
||||
var it = self.map.iterator();
|
||||
while (it.next()) |entry| {
|
||||
const value = other.map.get(entry.key_ptr.*) orelse return false;
|
||||
if (!std.mem.eql(u8, entry.value_ptr.*, value)) return false;
|
||||
} else return true;
|
||||
}
|
||||
|
||||
/// Used by formatter
|
||||
pub fn formatEntry(self: RepeatableStringMap, formatter: anytype) !void {
|
||||
// If no items, we want to render an empty field.
|
||||
if (self.map.count() == 0) {
|
||||
try formatter.formatEntry(void, {});
|
||||
return;
|
||||
}
|
||||
|
||||
var it = self.map.iterator();
|
||||
while (it.next()) |entry| {
|
||||
var buf: [256]u8 = undefined;
|
||||
const value = std.fmt.bufPrint(&buf, "{s}={s}", .{ entry.key_ptr.*, entry.value_ptr.* }) catch |err| switch (err) {
|
||||
error.NoSpaceLeft => return error.OutOfMemory,
|
||||
};
|
||||
try formatter.formatEntry([]const u8, value);
|
||||
}
|
||||
}
|
||||
|
||||
test "RepeatableStringMap: parseCLI" {
|
||||
const testing = std.testing;
|
||||
var arena = std.heap.ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
var map: RepeatableStringMap = .{};
|
||||
|
||||
try testing.expectError(error.ValueRequired, map.parseCLI(alloc, "A"));
|
||||
|
||||
try map.parseCLI(alloc, "A=B");
|
||||
try map.parseCLI(alloc, "B=C");
|
||||
try testing.expectEqual(@as(usize, 2), map.count());
|
||||
|
||||
try map.parseCLI(alloc, "");
|
||||
try testing.expectEqual(@as(usize, 0), map.count());
|
||||
|
||||
try map.parseCLI(alloc, "A=B");
|
||||
try testing.expectEqual(@as(usize, 1), map.count());
|
||||
try map.parseCLI(alloc, "A=C");
|
||||
try testing.expectEqual(@as(usize, 1), map.count());
|
||||
}
|
||||
|
||||
test "RepeatableStringMap: formatConfig empty" {
|
||||
const testing = std.testing;
|
||||
var buf = std.ArrayList(u8).init(testing.allocator);
|
||||
defer buf.deinit();
|
||||
|
||||
var list: RepeatableStringMap = .{};
|
||||
try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer()));
|
||||
try std.testing.expectEqualSlices(u8, "a = \n", buf.items);
|
||||
}
|
||||
|
||||
test "RepeatableStringMap: formatConfig single item" {
|
||||
const testing = std.testing;
|
||||
|
||||
var arena = std.heap.ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
{
|
||||
var buf = std.ArrayList(u8).init(testing.allocator);
|
||||
defer buf.deinit();
|
||||
var map: RepeatableStringMap = .{};
|
||||
try map.parseCLI(alloc, "A=B");
|
||||
try map.formatEntry(formatterpkg.entryFormatter("a", buf.writer()));
|
||||
try std.testing.expectEqualSlices(u8, "a = A=B\n", buf.items);
|
||||
}
|
||||
{
|
||||
var buf = std.ArrayList(u8).init(testing.allocator);
|
||||
defer buf.deinit();
|
||||
var map: RepeatableStringMap = .{};
|
||||
try map.parseCLI(alloc, " A = B ");
|
||||
try map.formatEntry(formatterpkg.entryFormatter("a", buf.writer()));
|
||||
try std.testing.expectEqualSlices(u8, "a = A=B\n", buf.items);
|
||||
}
|
||||
}
|
||||
|
||||
test "RepeatableStringMap: formatConfig multiple items" {
|
||||
const testing = std.testing;
|
||||
|
||||
var arena = std.heap.ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
{
|
||||
var buf = std.ArrayList(u8).init(testing.allocator);
|
||||
defer buf.deinit();
|
||||
var list: RepeatableStringMap = .{};
|
||||
try list.parseCLI(alloc, "A=B");
|
||||
try list.parseCLI(alloc, "B = C");
|
||||
try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer()));
|
||||
try std.testing.expectEqualSlices(u8, "a = A=B\na = B=C\n", buf.items);
|
||||
}
|
||||
}
|
@ -683,6 +683,7 @@ pub const ThreadData = struct {
|
||||
pub const Config = struct {
|
||||
command: ?[]const u8 = null,
|
||||
env: EnvMap,
|
||||
env_override: configpkg.RepeatableStringMap = .{},
|
||||
shell_integration: configpkg.Config.ShellIntegration = .detect,
|
||||
shell_integration_features: configpkg.Config.ShellIntegrationFeatures = .{},
|
||||
working_directory: ?[]const u8 = null,
|
||||
@ -889,6 +890,15 @@ const Subprocess = struct {
|
||||
log.warn("shell could not be detected, no automatic shell integration will be injected", .{});
|
||||
}
|
||||
|
||||
// Add the environment variables that override any others.
|
||||
{
|
||||
var it = cfg.env_override.iterator();
|
||||
while (it.next()) |entry| try env.put(
|
||||
entry.key_ptr.*,
|
||||
entry.value_ptr.*,
|
||||
);
|
||||
}
|
||||
|
||||
// Build our args list
|
||||
const args = args: {
|
||||
const cap = 9; // the most we'll ever use
|
||||
|
Reference in New Issue
Block a user