Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

core: add env config option #5309

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Surface.zig
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,7 @@ pub fn init(
// Initialize our IO backend
var io_exec = try termio.Exec.init(alloc, .{
.command = command,
.env = config.env,
.shell_integration = config.@"shell-integration",
.shell_integration_features = config.@"shell-integration-features",
.working_directory = config.@"working-directory",
Expand Down
1 change: 1 addition & 0 deletions src/config.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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 = Config.RepeatableStringMap;
pub const RepeatablePath = Config.RepeatablePath;
pub const ShellIntegrationFeatures = Config.ShellIntegrationFeatures;
pub const WindowPaddingColor = Config.WindowPaddingColor;
Expand Down
32 changes: 32 additions & 0 deletions src/config/Config.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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");
pub const RepeatableStringMap = @import("RepeatableStringMap.zig");

const log = std.log.scoped(.config);

Expand Down Expand Up @@ -730,6 +731,37 @@ command: ?[]const u8 = null,
///
@"initial-command": ?[]const u8 = null,

/// 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.
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
Expand Down
190 changes: 190 additions & 0 deletions src/config/RepeatableStringMap.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
/// 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 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: std.mem.Allocator, input: ?[]const u8) !void {
const value = input orelse return error.ValueRequired;

// Empty value resets the list
if (value.len == 0) {
var it = self.map.iterator();
while (it.next()) |entry| {
alloc.free(entry.key_ptr.*);
alloc.free(entry.value_ptr.*);
}
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);

if (val.len == 0) {
if (self.map.fetchOrderedRemove(key_copy)) |entry| {
alloc.free(entry.key);
alloc.free(entry.value);
}
alloc.free(key_copy);
return;
}

const val_copy = try alloc.dupeZ(u8, val);
errdefer alloc.free(val_copy);

if (try self.map.fetchPut(alloc, key_copy, val_copy)) |entry| {
alloc.free(key_copy);
alloc.free(entry.value);
}
}

/// Deep copy of the struct. Required by Config.
pub fn clone(self: *const RepeatableStringMap, alloc: std.mem.Allocator) std.mem.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);
}
}
13 changes: 13 additions & 0 deletions src/termio/Exec.zig
Original file line number Diff line number Diff line change
Expand Up @@ -673,6 +673,7 @@ pub const ThreadData = struct {

pub const Config = struct {
command: ?[]const u8 = null,
env: configpkg.RepeatableStringMap = .{},
shell_integration: configpkg.Config.ShellIntegration = .detect,
shell_integration_features: configpkg.Config.ShellIntegrationFeatures = .{},
working_directory: ?[]const u8 = null,
Expand Down Expand Up @@ -867,6 +868,18 @@ const Subprocess = struct {
env.remove("GSK_RENDERER");
}

// Add environment variables from the config
{
var it = cfg.env.iterator();
while (it.next()) |entry| {
const key_copy = try alloc.dupe(u8, entry.key_ptr.*);
errdefer alloc.free(key_copy);
const value_copy = try alloc.dupe(u8, entry.value_ptr.*);
errdefer alloc.free(value_copy);
try env.put(key_copy, value_copy);
}
}

// Setup our shell integration, if we can.
const integrated_shell: ?shell_integration.Shell, const shell_command: []const u8 = shell: {
const default_shell_command = cfg.command orelse switch (builtin.os.tag) {
Expand Down