Skip to content

Commit d4d54ce

Browse files
Move environment map to a new class, load once
1 parent 9b77cb3 commit d4d54ce

12 files changed

Lines changed: 214 additions & 133 deletions

File tree

examples/file_browser.zig

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@ const Model = struct {
1616
pub fn init(self: *Model, ctx: *zz.Context) zz.Cmd(Msg) {
1717
self.file_picker = zz.components.FilePicker.init(ctx.persistent_allocator);
1818
self.file_picker.height = ctx.height -| 10;
19+
self.file_picker.setHomePath(ctx.home_dir);
1920

2021
// Start at home directory
21-
self.file_picker.navigateHome(ctx.io, ctx.environ_map) catch {
22+
self.file_picker.navigateHome(ctx.io) catch {
2223
self.file_picker.navigate(ctx.io, "/") catch {};
2324
};
2425

@@ -34,15 +35,15 @@ const Model = struct {
3435
.char => |c| switch (c) {
3536
'q' => return .quit,
3637
else => {
37-
const selected = self.file_picker.handleKey(ctx.io, ctx.environ_map, k) catch false;
38+
const selected = self.file_picker.handleKey(ctx.io, k) catch false;
3839
if (selected) {
3940
self.loadPreview(ctx.io);
4041
}
4142
},
4243
},
4344
.escape => return .quit,
4445
else => {
45-
const selected = self.file_picker.handleKey(ctx.io, ctx.environ_map, k) catch false;
46+
const selected = self.file_picker.handleKey(ctx.io, k) catch false;
4647
if (selected) {
4748
self.loadPreview(ctx.io);
4849
}

examples/theming.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const Model = struct {
1515
};
1616

1717
pub fn init(self: *Model, ctx: *zz.Context) zz.Cmd(Msg) {
18-
self.tm = zz.ThemeManager.init(ctx.environ_map);
18+
self.tm = zz.ThemeManager.init(ctx.is_dark_background);
1919
self.progress_val = 35;
2020
// Also set the theme on the context so components can read it
2121
ctx.setTheme(self.tm.current.palette);

src/components/file_picker.zig

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ pub const FilePicker = struct {
3030
dir_only: bool,
3131
file_only: bool,
3232
allowed_extensions: ?[]const []const u8,
33+
home_path: []const u8,
3334

3435
// Styling
3536
dir_style: style_mod.Style,
@@ -76,6 +77,7 @@ pub const FilePicker = struct {
7677
.dir_only = false,
7778
.file_only = false,
7879
.allowed_extensions = null,
80+
.home_path = defaultHomePath(),
7981
.dir_style = blk: {
8082
var s = style_mod.Style{};
8183
s = s.bold(true);
@@ -225,15 +227,13 @@ pub const FilePicker = struct {
225227
self.y_offset = 0;
226228
}
227229

228-
/// Navigate to home directory using the supplied environment.
229-
pub fn navigateHome(self: *FilePicker, io: std.Io, environ_map: *const std.process.Environ.Map) !void {
230-
if (comptime builtin.os.tag == .windows) {
231-
const home = environ_map.get("USERPROFILE") orelse "C:\\";
232-
try self.navigate(io, home);
233-
} else {
234-
const home = environ_map.get("HOME") orelse "/";
235-
try self.navigate(io, home);
236-
}
230+
pub fn setHomePath(self: *FilePicker, home_path: []const u8) void {
231+
self.home_path = if (home_path.len > 0) home_path else defaultHomePath();
232+
}
233+
234+
/// Navigate to the configured home directory.
235+
pub fn navigateHome(self: *FilePicker, io: std.Io) !void {
236+
try self.navigate(io, self.home_path);
237237
}
238238

239239
/// Move cursor up
@@ -295,7 +295,6 @@ pub const FilePicker = struct {
295295
pub fn handleKey(
296296
self: *FilePicker,
297297
io: std.Io,
298-
environ_map: *const std.process.Environ.Map,
299298
key: keys.KeyEvent,
300299
) !bool {
301300
if (!self.focused) return false;
@@ -314,7 +313,7 @@ pub const FilePicker = struct {
314313
'j' => self.cursorDown(),
315314
'k' => self.cursorUp(),
316315
'h' => self.showHidden(io),
317-
'~' => try self.navigateHome(io, environ_map),
316+
'~' => try self.navigateHome(io),
318317
else => {},
319318
}
320319
},
@@ -330,6 +329,10 @@ pub const FilePicker = struct {
330329
self.navigate(io, path_copy) catch {};
331330
}
332331

332+
fn defaultHomePath() []const u8 {
333+
return if (comptime builtin.os.tag == .windows) "C:\\" else "/";
334+
}
335+
333336
fn ensureVisible(self: *FilePicker) void {
334337
if (self.cursor < self.y_offset) {
335338
self.y_offset = self.cursor;

src/core/context.zig

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const color_mod = @import("../style/color.zig");
99
const unicode_mod = @import("../unicode.zig");
1010
const Logger = @import("log.zig").Logger;
1111
const theme_mod = @import("../style/theme.zig");
12+
const Environment = @import("environment.zig").Environment;
1213

1314
/// Runtime context passed to init, update, and view functions
1415
pub const Context = struct {
@@ -18,8 +19,8 @@ pub const Context = struct {
1819
/// Persistent allocator for model state (not reset between frames)
1920
persistent_allocator: std.mem.Allocator,
2021

21-
/// Process environment for env-driven detection (color, multiplexer, locale).
22-
environ_map: *const std.process.Environ.Map,
22+
/// Home directory captured from the process environment at startup.
23+
home_dir: []const u8,
2324

2425
/// Asynchronous I/O facilities (file, network, time, sleep).
2526
io: std.Io,
@@ -80,14 +81,14 @@ pub const Context = struct {
8081
allocator: std.mem.Allocator,
8182
persistent_allocator: std.mem.Allocator,
8283
io: std.Io,
83-
environ_map: *const std.process.Environ.Map,
84+
environment: *const Environment,
8485
) Context {
85-
const profile = color_mod.ColorProfile.detect(environ_map);
86+
const profile = environment.color_profile;
8687
return .{
8788
.allocator = allocator,
8889
.persistent_allocator = persistent_allocator,
8990
.io = io,
90-
.environ_map = environ_map,
91+
.home_dir = environment.home_dir,
9192
.width = 80,
9293
.height = 24,
9394
.frame = 0,
@@ -96,7 +97,7 @@ pub const Context = struct {
9697
.true_color = profile.supportsTrueColor(),
9798
.color_256 = profile.supports256(),
9899
.color_profile = profile,
99-
.is_dark_background = color_mod.hasDarkBackground(environ_map),
100+
.is_dark_background = environment.is_dark_background,
100101
.unicode_width_strategy = unicode_mod.getWidthStrategy(),
101102
.terminal_mode_2027 = false,
102103
.kitty_text_sizing = false,

src/core/environment.zig

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
//! Process environment values captured at program startup.
2+
3+
const std = @import("std");
4+
const builtin = @import("builtin");
5+
const color_mod = @import("../style/color.zig");
6+
const unicode_mod = @import("../unicode.zig");
7+
8+
pub const Environment = struct {
9+
term: []const u8 = "",
10+
term_program: []const u8 = "",
11+
lc_terminal: []const u8 = "",
12+
color_term: []const u8 = "",
13+
color_fg_bg: []const u8 = "",
14+
term_features: []const u8 = "",
15+
home_dir: []const u8 = defaultHomeDir(),
16+
no_color: bool = false,
17+
has_tmux: bool = false,
18+
has_zellij: bool = false,
19+
has_kitty_window: bool = false,
20+
color_profile: color_mod.ColorProfile = .ansi,
21+
is_dark_background: bool = true,
22+
unicode_width_override: ?unicode_mod.WidthStrategy = null,
23+
24+
pub fn fromEnvMap(environ_map: *const std.process.Environ.Map) Environment {
25+
const term = environ_map.get("TERM") orelse "";
26+
const term_program = environ_map.get("TERM_PROGRAM") orelse "";
27+
const lc_terminal = environ_map.get("LC_TERMINAL") orelse "";
28+
const color_term = environ_map.get("COLORTERM") orelse "";
29+
const color_fg_bg = environ_map.get("COLORFGBG") orelse "";
30+
const term_features = environ_map.get("TERM_FEATURES") orelse "";
31+
const home_dir = homeDirFromEnvMap(environ_map);
32+
const no_color = environ_map.get("NO_COLOR") != null;
33+
34+
return .{
35+
.term = term,
36+
.term_program = term_program,
37+
.lc_terminal = lc_terminal,
38+
.color_term = color_term,
39+
.color_fg_bg = color_fg_bg,
40+
.term_features = term_features,
41+
.home_dir = home_dir,
42+
.no_color = no_color,
43+
.has_tmux = envValuePresent(environ_map, "TMUX"),
44+
.has_zellij = envValuePresent(environ_map, "ZELLIJ"),
45+
.has_kitty_window = envValuePresent(environ_map, "KITTY_WINDOW_ID"),
46+
.color_profile = color_mod.ColorProfile.detect(.{
47+
.no_color = no_color,
48+
.color_term = color_term,
49+
.term = term,
50+
}),
51+
.is_dark_background = color_mod.hasDarkBackground(color_fg_bg),
52+
.unicode_width_override = parseUnicodeWidthOverride(environ_map.get("ZZ_UNICODE_WIDTH") orelse ""),
53+
};
54+
}
55+
56+
pub fn isInsideMultiplexer(self: *const Environment) bool {
57+
return self.has_tmux or self.has_zellij or self.termContains("screen");
58+
}
59+
60+
pub fn isKnownUnicodeWidthTerminal(self: *const Environment) bool {
61+
return self.termProgramEquals("WezTerm") or
62+
self.termProgramEquals("iTerm.app") or
63+
self.termContains("wezterm") or
64+
self.termContains("ghostty");
65+
}
66+
67+
pub fn looksLikeKittyTerminal(self: *const Environment) bool {
68+
return self.has_kitty_window or self.termContains("kitty");
69+
}
70+
71+
pub fn looksLikeIterm2Terminal(self: *const Environment) bool {
72+
return self.termProgramEquals("iTerm.app") or self.lcTerminalEquals("iTerm2");
73+
}
74+
75+
pub fn looksLikeSixelTerminal(self: *const Environment) bool {
76+
return self.termContains("sixel") or
77+
self.termContains("mlterm") or
78+
self.termContains("yaft") or
79+
self.termContains("contour");
80+
}
81+
82+
pub fn termContains(self: *const Environment, needle: []const u8) bool {
83+
return std.mem.indexOf(u8, self.term, needle) != null;
84+
}
85+
86+
pub fn termProgramEquals(self: *const Environment, expected: []const u8) bool {
87+
return std.ascii.eqlIgnoreCase(self.term_program, expected);
88+
}
89+
90+
pub fn lcTerminalEquals(self: *const Environment, expected: []const u8) bool {
91+
return std.ascii.eqlIgnoreCase(self.lc_terminal, expected);
92+
}
93+
94+
fn homeDirFromEnvMap(environ_map: *const std.process.Environ.Map) []const u8 {
95+
if (comptime builtin.os.tag == .windows) {
96+
if (environ_map.get("USERPROFILE")) |home| {
97+
if (home.len > 0) return home;
98+
}
99+
} else {
100+
if (environ_map.get("HOME")) |home| {
101+
if (home.len > 0) return home;
102+
}
103+
}
104+
return defaultHomeDir();
105+
}
106+
107+
fn defaultHomeDir() []const u8 {
108+
return if (comptime builtin.os.tag == .windows) "C:\\" else "/";
109+
}
110+
111+
fn envValuePresent(environ_map: *const std.process.Environ.Map, name: []const u8) bool {
112+
const value = environ_map.get(name) orelse return false;
113+
return value.len > 0;
114+
}
115+
116+
fn parseUnicodeWidthOverride(raw: []const u8) ?unicode_mod.WidthStrategy {
117+
if (std.ascii.eqlIgnoreCase(raw, "unicode")) return .unicode;
118+
if (std.ascii.eqlIgnoreCase(raw, "legacy")) return .legacy_wcwidth;
119+
if (std.ascii.eqlIgnoreCase(raw, "auto")) return null;
120+
return null;
121+
}
122+
};

src/core/program.zig

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const message = @import("message.zig");
1212
const command = @import("command.zig");
1313
const Logger = @import("log.zig").Logger;
1414
const unicode = @import("../unicode.zig");
15+
const Environment = @import("environment.zig").Environment;
1516

1617
pub const Cmd = command.Cmd;
1718
pub const Msg = message;
@@ -47,7 +48,7 @@ pub fn Program(comptime Model: type) type {
4748
return struct {
4849
allocator: std.mem.Allocator,
4950
io: std.Io,
50-
environ_map: *const std.process.Environ.Map,
51+
environment: Environment,
5152
arena: std.heap.ArenaAllocator,
5253
model: Model,
5354
terminal: ?Terminal,
@@ -72,7 +73,7 @@ pub fn Program(comptime Model: type) type {
7273

7374
const Self = @This();
7475

75-
/// Initialize the program
76+
/// Initialize the program.
7677
pub fn init(
7778
allocator: std.mem.Allocator,
7879
io: std.Io,
@@ -81,7 +82,7 @@ pub fn Program(comptime Model: type) type {
8182
return initWithOptions(allocator, io, environ_map, .{});
8283
}
8384

84-
/// Initialize with custom options
85+
/// Initialize with custom options.
8586
pub fn initWithOptions(
8687
allocator: std.mem.Allocator,
8788
io: std.Io,
@@ -90,16 +91,14 @@ pub fn Program(comptime Model: type) type {
9091
) !Self {
9192
const arena = std.heap.ArenaAllocator.init(allocator);
9293
const clock_epoch = std.Io.Clock.Timestamp.now(io, .boot);
93-
const self = Self{
94+
var self = Self{
9495
.allocator = allocator,
9596
.io = io,
96-
.environ_map = environ_map,
97+
.environment = .fromEnvMap(environ_map),
9798
.arena = arena,
9899
.model = undefined,
99100
.terminal = null,
100-
// `self` is returned by value, so don't capture an arena allocator here.
101-
// It would point at this function's stack copy and dangle after return.
102-
.context = Context.init(allocator, allocator, io, environ_map),
101+
.context = undefined,
103102
.options = options,
104103
.running = false,
105104
.clock_epoch = clock_epoch,
@@ -114,6 +113,10 @@ pub fn Program(comptime Model: type) type {
114113
.filter = null,
115114
};
116115

116+
// `self` is returned by value, so don't capture an arena allocator here.
117+
// It would point at this function's stack copy and dangle after return.
118+
self.context = Context.init(allocator, allocator, io, &self.environment);
119+
117120
return self;
118121
}
119122

@@ -171,7 +174,7 @@ pub fn Program(comptime Model: type) type {
171174
}
172175

173176
// Initialize terminal
174-
self.terminal = try Terminal.init(self.io, self.environ_map, .{
177+
self.terminal = try Terminal.init(self.io, &self.environment, .{
175178
.alt_screen = self.options.alt_screen,
176179
.hide_cursor = !self.options.cursor,
177180
.mouse = self.options.mouse,
@@ -372,20 +375,12 @@ pub fn Program(comptime Model: type) type {
372375
if (self.options.unicode_width_strategy) |forced| {
373376
return forced;
374377
}
375-
if (envUnicodeWidthOverride(self.environ_map)) |from_env| {
378+
if (self.environment.unicode_width_override) |from_env| {
376379
return from_env;
377380
}
378381
return detected;
379382
}
380383

381-
fn envUnicodeWidthOverride(environ_map: *const std.process.Environ.Map) ?unicode.WidthStrategy {
382-
const raw = environ_map.get("ZZ_UNICODE_WIDTH") orelse return null;
383-
if (std.ascii.eqlIgnoreCase(raw, "unicode")) return .unicode;
384-
if (std.ascii.eqlIgnoreCase(raw, "legacy")) return .legacy_wcwidth;
385-
if (std.ascii.eqlIgnoreCase(raw, "auto")) return null;
386-
return null;
387-
}
388-
389384
fn processMouseEvent(self: *Self, mouse_event: keyboard.MouseEvent) ?UserCmd {
390385
if (@hasField(UserMsg, "mouse")) {
391386
const user_msg = UserMsg{ .mouse = mouse_event };

src/root.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ pub const program = @import("core/program.zig");
5252
pub const Program = program.Program;
5353
pub const Cmd = program.Cmd;
5454
pub const command = @import("core/command.zig");
55+
pub const Environment = @import("core/environment.zig").Environment;
5556
pub const async_task = @import("core/async_task.zig");
5657
pub const AsyncRunner = async_task.AsyncRunner;
5758
pub const SubProgram = @import("core/sub_program.zig").SubProgram;

0 commit comments

Comments
 (0)