From 320dc35d0a9925ca77c0f5f3583c420053be4605 Mon Sep 17 00:00:00 2001 From: Rohit-Bevinahally Date: Mon, 20 Jan 2025 01:25:08 +0530 Subject: [PATCH] commons --- src/config.zig | 2 + src/config/Config.zig | 463 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 455 insertions(+), 10 deletions(-) diff --git a/src/config.zig b/src/config.zig index fb7359b3e..2eeeb29cf 100644 --- a/src/config.zig +++ b/src/config.zig @@ -30,8 +30,10 @@ pub const RepeatableFontVariation = Config.RepeatableFontVariation; pub const RepeatableString = Config.RepeatableString; pub const RepeatableStringMap = @import("config/RepeatableStringMap.zig"); pub const RepeatablePath = Config.RepeatablePath; +pub const SinglePath = Config.SinglePath; pub const ShellIntegrationFeatures = Config.ShellIntegrationFeatures; pub const WindowPaddingColor = Config.WindowPaddingColor; +pub const BackgroundImageMode = Config.BackgroundImageMode; // Alternate APIs pub const CAPI = @import("config/CAPI.zig"); diff --git a/src/config/Config.zig b/src/config/Config.zig index 344c118d7..880105634 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -459,6 +459,32 @@ background: Color = .{ .r = 0x28, .g = 0x2C, .b = 0x34 }, /// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color. foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, +/// Background image for the window. +@"background-image": SinglePath = .{}, + +/// Background image opacity +@"background-image-opacity": f32 = 1.0, + +/// Background image mode to use. +/// +/// Valid values are: +/// +/// * `zoomed` - Image is scaled to fit the window, preserving aspect ratio. +/// * `scaled` - Image is scaled to fill the window, not preserving aspect ratio. +/// * `tiled` - Image is repeated horizontally and vertically to fill the window. +/// * `centered` - Image is centered in the window and displayed 1-to-1 pixel +/// scale, preserving both the aspect ratio and the image size. +/// * `upper-left` - Image is anchored to the upper left corner of the window, +/// preserving the aspect ratio. +/// * `upper-right` - Image is anchored to the upper right corner of the window, +/// preserving the aspect ratio. +/// * `lower-left` - Image is anchored to the lower left corner of the window, +/// preserving the aspect ratio. +/// * `lower-right` - Image is anchored to the lower right corner of the window, +/// preserving the aspect ratio. +/// +@"background-image-mode": BackgroundImageMode = .zoomed, + /// The foreground and background color for selection. If this is not set, then /// the selection color is just the inverted window background and foreground /// (note: not to be confused with the cell bg/fg). @@ -2975,20 +3001,97 @@ fn expandPaths(self: *Config, base: []const u8) !void { ); // Expand all of our paths - inline for (@typeInfo(Config).@"struct".fields) |field| { - switch (field.type) { - RepeatablePath, Path => { - try @field(self, field.name).expand( - arena_alloc, - base, - &self._diagnostics, - ); - }, - else => {}, + inline for (@typeInfo(Config).Struct.fields) |field| { + if (field.type == RepeatablePath) { + try @field(self, field.name).expand( + arena_alloc, + base, + &self._diagnostics, + ); } } } +/// Expand a relative path to an absolute path. This function is used by +/// the RepeatablePath and SinglePath to expand the paths they store. +fn expandPath( + alloc: Allocator, + base: []const u8, + path: []const u8, + diags: *cli.DiagnosticList, +) ![]const u8 { + assert(std.fs.path.isAbsolute(base)); + var dir = try std.fs.cwd().openDir(base, .{}); + defer dir.close(); + + // If it is already absolute we can just return it + if (path.len == 0 or std.fs.path.isAbsolute(path)) return path; + + // If it isn't absolute, we need to make it absolute relative + // to the base. + var buf: [std.fs.max_path_bytes]u8 = undefined; + + // Check if the path starts with a tilde and expand it to the + // home directory on Linux/macOS. We explicitly look for "~/" + // because we don't support alternate users such as "~alice/" + if (std.mem.startsWith(u8, path, "~/")) expand: { + // Windows isn't supported yet + if (comptime builtin.os.tag == .windows) break :expand; + + const expanded: []const u8 = internal_os.expandHome( + path, + &buf, + ) catch |err| { + try diags.append(alloc, .{ + .message = try std.fmt.allocPrintZ( + alloc, + "error expanding home directory for path {s}: {}", + .{ path, err }, + ), + }); + + // We can't expand this path so return an empty string + return ""; + }; + + log.debug( + "expanding file path from home directory: path={s}", + .{expanded}, + ); + + return expanded; + } + + const abs = dir.realpath(path, &buf) catch |err| abs: { + if (err == error.FileNotFound) { + // The file doesn't exist. Try to resolve the relative path + // another way. + const resolved = try std.fs.path.resolve(alloc, &.{ base, path }); + defer alloc.free(resolved); + @memcpy(buf[0..resolved.len], resolved); + break :abs buf[0..resolved.len]; + } + + try diags.append(alloc, .{ + .message = try std.fmt.allocPrintZ( + alloc, + "error resolving file path {s}: {}", + .{ path, err }, + ), + }); + + // We can't expand this path so return an empty string + return ""; + }; + + log.debug( + "expanding file path relative={s} abs={s}", + .{ path, abs }, + ); + + return abs; +} + fn loadTheme(self: *Config, theme: Theme) !void { // Load the correct theme depending on the conditional state. // Dark/light themes were programmed prior to conditional configuration @@ -4167,6 +4270,63 @@ pub const Palette = struct { } }; +/// SinglePath is a path to a single file. When loading the configuration +/// file, always the last one will be kept and be automatically expanded +/// relative to the path of the config file. +pub const SinglePath = struct { + const Self = @This(); + + /// The actual value that is updated as we parse. + value: ?[]const u8 = null, + + /// Parse a single path. + pub fn parseCLI(self: *Self, alloc: Allocator, input: ?[]const u8) !void { + const value = input orelse return error.ValueRequired; + // If the value is empty, we set the value to null + if (value.len == 0) { + self.value = null; + return; + } + const copy = try alloc.dupe(u8, value); + self.value = copy; + } + + /// Deep copy of the struct. Required by Config. + pub fn clone(self: Self, alloc: Allocator) Allocator.Error!Self { + const value = self.value orelse return .{}; + + const copy_path = try alloc.dupe(u8, value); + return .{ + .value = copy_path, + }; + } + + /// Used by Formatter + pub fn formatEntry(self: Self, formatter: anytype) !void { + const value = self.value orelse return; + try formatter.formatEntry([]const u8, value); + } + + /// Expand all the paths relative to the base directory. + pub fn expand( + self: *Self, + alloc: Allocator, + base: []const u8, + diags: *cli.DiagnosticList, + ) !void { + // Try expanding path relative to the base. + const path = self.value orelse return; + const abs = try expandPath(alloc, base, path, diags); + + if (abs.len == 0) { + // Blank this path so that we don't attempt to resolve it again + self.value = null; + return; + } + self.value = try alloc.dupeZ(u8, abs); + } +}; + /// RepeatableString is a string value that can be repeated to accumulate /// a list of strings. This isn't called "StringList" because I find that /// sometimes leads to confusion that it _accepts_ a list such as @@ -4323,6 +4483,272 @@ pub const RepeatableString = struct { } }; +/// RepeatablePath is like repeatable string but represents a path value. +/// The difference is that when loading the configuration any values for +/// this will be automatically expanded relative to the path of the config +/// file. +pub const RepeatablePath = struct { + const Self = @This(); + + const Path = union(enum) { + /// No error if the file does not exist. + optional: [:0]const u8, + + /// The file is required to exist. + required: [:0]const u8, + }; + + value: std.ArrayListUnmanaged(Path) = .{}, + + pub fn parseCLI(self: *Self, alloc: Allocator, input: ?[]const u8) !void { + const value, const optional = if (input) |value| blk: { + if (value.len == 0) { + self.value.clearRetainingCapacity(); + return; + } + + break :blk if (value[0] == '?') + .{ value[1..], true } + else if (value.len >= 2 and value[0] == '"' and value[value.len - 1] == '"') + .{ value[1 .. value.len - 1], false } + else + .{ value, false }; + } else return error.ValueRequired; + + if (value.len == 0) { + // This handles the case of zero length paths after removing any ? + // prefixes or surrounding quotes. In this case, we don't reset the + // list. + return; + } + + const item: Path = if (optional) + .{ .optional = try alloc.dupeZ(u8, value) } + else + .{ .required = try alloc.dupeZ(u8, value) }; + + try self.value.append(alloc, item); + } + + /// Deep copy of the struct. Required by Config. + pub fn clone(self: *const Self, alloc: Allocator) Allocator.Error!Self { + const value = try self.value.clone(alloc); + for (value.items) |*item| { + switch (item.*) { + .optional, .required => |*path| path.* = try alloc.dupeZ(u8, path.*), + } + } + + return .{ + .value = value, + }; + } + + /// Compare if two of our value are requal. Required by Config. + pub fn equal(self: Self, other: Self) bool { + if (self.value.items.len != other.value.items.len) return false; + for (self.value.items, other.value.items) |a, b| { + if (!std.meta.eql(a, b)) return false; + } + + return true; + } + + /// Used by Formatter + pub fn formatEntry(self: Self, formatter: anytype) !void { + if (self.value.items.len == 0) { + try formatter.formatEntry(void, {}); + return; + } + + var buf: [std.fs.max_path_bytes + 1]u8 = undefined; + for (self.value.items) |item| { + const value = switch (item) { + .optional => |path| std.fmt.bufPrint( + &buf, + "?{s}", + .{path}, + ) catch |err| switch (err) { + // Required for builds on Linux where NoSpaceLeft + // isn't an allowed error for fmt. + error.NoSpaceLeft => return error.OutOfMemory, + }, + .required => |path| path, + }; + + try formatter.formatEntry([]const u8, value); + } + } + + /// Expand all the paths relative to the base directory. + pub fn expand( + self: *Self, + alloc: Allocator, + base: []const u8, + diags: *cli.DiagnosticList, + ) !void { + assert(std.fs.path.isAbsolute(base)); + var dir = try std.fs.cwd().openDir(base, .{}); + defer dir.close(); + + for (0..self.value.items.len) |i| { + const path = switch (self.value.items[i]) { + .optional, .required => |path| path, + }; + + // If it is already absolute we can ignore it. + if (path.len == 0 or std.fs.path.isAbsolute(path)) continue; + + // If it isn't absolute, we need to make it absolute relative + // to the base. + var buf: [std.fs.max_path_bytes]u8 = undefined; + + // Check if the path starts with a tilde and expand it to the + // home directory on Linux/macOS. We explicitly look for "~/" + // because we don't support alternate users such as "~alice/" + if (std.mem.startsWith(u8, path, "~/")) expand: { + // Windows isn't supported yet + if (comptime builtin.os.tag == .windows) break :expand; + + const expanded: []const u8 = internal_os.expandHome( + path, + &buf, + ) catch |err| { + try diags.append(alloc, .{ + .message = try std.fmt.allocPrintZ( + alloc, + "error expanding home directory for path {s}: {}", + .{ path, err }, + ), + }); + + // Blank this path so that we don't attempt to resolve it + // again + self.value.items[i] = .{ .required = "" }; + + continue; + }; + + log.debug( + "expanding file path from home directory: path={s}", + .{expanded}, + ); + + switch (self.value.items[i]) { + .optional, .required => |*p| p.* = try alloc.dupeZ(u8, expanded), + } + + continue; + } + + const abs = dir.realpath(path, &buf) catch |err| abs: { + if (err == error.FileNotFound) { + // The file doesn't exist. Try to resolve the relative path + // another way. + const resolved = try std.fs.path.resolve(alloc, &.{ base, path }); + defer alloc.free(resolved); + @memcpy(buf[0..resolved.len], resolved); + break :abs buf[0..resolved.len]; + } + + try diags.append(alloc, .{ + .message = try std.fmt.allocPrintZ( + alloc, + "error resolving file path {s}: {}", + .{ path, err }, + ), + }); + + // Blank this path so that we don't attempt to resolve it again + self.value.items[i] = .{ .required = "" }; + + continue; + }; + + log.debug( + "expanding file path relative={s} abs={s}", + .{ path, abs }, + ); + + switch (self.value.items[i]) { + .optional, .required => |*p| p.* = try alloc.dupeZ(u8, abs), + } + } + } + + test "parseCLI" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var list: Self = .{}; + try list.parseCLI(alloc, "config.1"); + try list.parseCLI(alloc, "?config.2"); + try list.parseCLI(alloc, "\"?config.3\""); + + // Zero-length values, ignored + try list.parseCLI(alloc, "?"); + try list.parseCLI(alloc, "\"\""); + + try testing.expectEqual(@as(usize, 3), list.value.items.len); + + const Tag = std.meta.Tag(Path); + try testing.expectEqual(Tag.required, @as(Tag, list.value.items[0])); + try testing.expectEqualStrings("config.1", list.value.items[0].required); + + try testing.expectEqual(Tag.optional, @as(Tag, list.value.items[1])); + try testing.expectEqualStrings("config.2", list.value.items[1].optional); + + try testing.expectEqual(Tag.required, @as(Tag, list.value.items[2])); + try testing.expectEqualStrings("?config.3", list.value.items[2].required); + + try list.parseCLI(alloc, ""); + try testing.expectEqual(@as(usize, 0), list.value.items.len); + } + + test "formatConfig empty" { + const testing = std.testing; + var buf = std.ArrayList(u8).init(testing.allocator); + defer buf.deinit(); + + var list: Self = .{}; + try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); + try std.testing.expectEqualSlices(u8, "a = \n", buf.items); + } + + test "formatConfig single item" { + const testing = std.testing; + var buf = std.ArrayList(u8).init(testing.allocator); + defer buf.deinit(); + + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var list: Self = .{}; + try list.parseCLI(alloc, "A"); + try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); + try std.testing.expectEqualSlices(u8, "a = A\n", buf.items); + } + + test "formatConfig multiple items" { + const testing = std.testing; + var buf = std.ArrayList(u8).init(testing.allocator); + defer buf.deinit(); + + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var list: Self = .{}; + try list.parseCLI(alloc, "A"); + try list.parseCLI(alloc, "?B"); + try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); + try std.testing.expectEqualSlices(u8, "a = A\na = ?B\n", buf.items); + } +}; + /// FontVariation is a repeatable configuration value that sets a single /// font variation value. Font variations are configurations for what /// are often called "variable fonts." The font files usually end in @@ -6199,6 +6625,23 @@ pub const AlphaBlending = enum { } }; +/// See background-image-mode +/// +/// This enum is used to set the background image mode. The shader expects +/// a `uint`, so we use `u8` here. The values for each mode should be kept +/// in sync with the values in the vertex shader used to render the +/// background image (`bgimage`). +pub const BackgroundImageMode = enum(u8) { + zoomed = 0, + stretched = 1, + tiled = 2, + centered = 3, + upper_left = 4, + upper_right = 5, + lower_left = 6, + lower_right = 7, +}; + /// See freetype-load-flag pub const FreetypeLoadFlags = packed struct { // The defaults here at the time of writing this match the defaults