diff --git a/src/main.zig b/src/main.zig index b864adf..35d4e96 100644 --- a/src/main.zig +++ b/src/main.zig @@ -498,9 +498,8 @@ var config_mutex: std.Thread.Mutex = .{}; pub fn exists_config(T: type) bool { config_mutex.lock(); defer config_mutex.unlock(); - const json_file_name = get_app_config_file_name(application_name, @typeName(T)) catch return false; - const text_file_name = json_file_name[0 .. json_file_name.len - ".json".len]; - var file = std.fs.openFileAbsolute(text_file_name, .{ .mode = .read_only }) catch return false; + const file_name = get_app_config_file_name(application_name, @typeName(T)) catch return false; + var file = std.fs.openFileAbsolute(file_name, .{ .mode = .read_only }) catch return false; defer file.close(); return true; } @@ -520,19 +519,15 @@ pub fn read_config(T: type, allocator: std.mem.Allocator) struct { T, [][]const config_mutex.lock(); defer config_mutex.unlock(); var bufs: [][]const u8 = &[_][]const u8{}; - const json_file_name = get_app_config_file_name(application_name, @typeName(T)) catch return .{ get_default(T), bufs }; - const text_file_name = json_file_name[0 .. json_file_name.len - ".json".len]; + const file_name = get_app_config_file_name(application_name, @typeName(T)) catch return .{ get_default(T), bufs }; var conf: T = get_default(T); - if (!read_config_file(T, allocator, &conf, &bufs, text_file_name)) { - _ = read_config_file(T, allocator, &conf, &bufs, json_file_name); - } + _ = read_config_file(T, allocator, &conf, &bufs, file_name); read_nested_include_files(T, allocator, &conf, &bufs); return .{ conf, bufs }; } // returns true if the file was found fn read_config_file(T: type, allocator: std.mem.Allocator, conf: *T, bufs: *[][]const u8, file_name: []const u8) bool { - // std.log.info("loading {s}", .{file_name}); const err: anyerror = blk: { if (std.mem.endsWith(u8, file_name, ".json")) if (read_json_config_file(T, allocator, conf, bufs, file_name)) return true else |e| break :blk e; if (read_text_config_file(T, allocator, conf, bufs, file_name)) return true else |e| break :blk e; @@ -672,16 +667,12 @@ fn read_nested_include_files(T: type, allocator: std.mem.Allocator, conf: *T, bu pub const ConfigWriteError = error{ CreateConfigFileFailed, WriteConfigFileFailed, WriteFailed }; -pub fn write_config(conf: anytype, allocator: std.mem.Allocator) (ConfigDirError || ConfigWriteError)!void { +pub fn write_config(data: anytype, allocator: std.mem.Allocator) (ConfigDirError || ConfigWriteError)!void { + const T = @TypeOf(data); config_mutex.lock(); defer config_mutex.unlock(); _ = allocator; - const file_name = try get_app_config_file_name(application_name, @typeName(@TypeOf(conf))); - return write_text_config_file(@TypeOf(conf), conf, file_name[0 .. file_name.len - 5]); - // return write_json_file(@TypeOf(conf), conf, allocator, try get_app_config_file_name(application_name, @typeName(@TypeOf(conf)))); -} - -fn write_text_config_file(comptime T: type, data: T, file_name: []const u8) ConfigWriteError!void { + const file_name = try get_app_config_file_name(application_name, @typeName(T)); var file = std.fs.createFileAbsolute(file_name, .{ .truncate = true }) catch |e| { std.log.err("createFileAbsolute failed with {any} for: {s}", .{ e, file_name }); return error.CreateConfigFileFailed; @@ -689,7 +680,20 @@ fn write_text_config_file(comptime T: type, data: T, file_name: []const u8) Conf defer file.close(); var buf: [4096]u8 = undefined; var writer = file.writer(&buf); - write_config_to_writer(T, data, &writer.interface) catch |e| { + + try writer.interface.print( + \\# This file is written by flow when settings are changed interactively. You may + \\# edit values by hand, but not comments. Comments will be overwritten. Values + \\# configured to the default value will be automatically commented and possibly + \\# updated if the default value changes. To store values that cannot be changed + \\# by flow, put them in a new file and reference it in the include_files + \\# configuration option at the end of this file. include_files are never + \\# modified by flow. + \\ + \\ + , .{}); + + write_config_to_writer_internal(T, data, &writer.interface) catch |e| { std.log.err("write file failed with {any} for: {s}", .{ e, file_name }); return error.WriteConfigFileFailed; }; @@ -697,8 +701,24 @@ fn write_text_config_file(comptime T: type, data: T, file_name: []const u8) Conf } pub fn write_config_to_writer(comptime T: type, data: T, writer: *std.Io.Writer) std.Io.Writer.Error!void { + try writer.print( + \\# This file is generated by flow and updated when opened by an interactive + \\# command. You may edit values by hand, but not comments. Comments will be + \\# overwritten. Values configured to the default value will be automatically + \\# commented and possibly updated if the default value changes. To store + \\# values that cannot be changed by flow, put them in a new file and reference + \\# it in the include_files configuration option at the end of this file. + \\# include_files are never modified by flow. + \\ + \\ + , .{}); + return write_config_to_writer_internal(T, data, writer); +} + +fn write_config_to_writer_internal(comptime T: type, data: T, writer: *std.Io.Writer) std.Io.Writer.Error!void { const default: T = .{}; inline for (@typeInfo(T).@"struct".fields) |field_info| { + var is_default = false; if (config_eql( T, field_info.type, @@ -706,29 +726,39 @@ pub fn write_config_to_writer(comptime T: type, data: T, writer: *std.Io.Writer) @field(default, field_info.name), )) { try writer.print("# {s} ", .{field_info.name}); + is_default = true; } else { try writer.print("{s} ", .{field_info.name}); } - switch (field_info.type) { - u24 => try write_color_value(@field(data, field_info.name), writer), - ?u24 => if (@field(data, field_info.name)) |value| - try write_color_value(value, writer) - else - try writer.writeAll("null"), - else => { - var s: std.json.Stringify = .{ .writer = writer, .options = .{ .whitespace = .minified } }; - try s.write(@field(data, field_info.name)); - }, - } + try write_config_value(field_info.type, @field(data, field_info.name), writer); try writer.print("\n", .{}); + if (!is_default) { + try writer.print("# default value: ", .{}); + try write_config_value(field_info.type, @field(default, field_info.name), writer); + try writer.print("\n", .{}); + } try writer.print("# value type: ", .{}); - try write_config_value_description(T, field_info.type, writer); + try write_config_value_description(T, field_info.type, field_info.name, writer); try writer.print("\n", .{}); try writer.print("\n", .{}); } } -fn write_config_value_description(T: type, field_type: type, writer: *std.Io.Writer) !void { +fn write_config_value(T: type, value: T, writer: *std.Io.Writer) !void { + switch (T) { + u24 => try write_color_value(value, writer), + ?u24 => if (value) |v| + try write_color_value(v, writer) + else + try writer.writeAll("null"), + else => { + var s: std.json.Stringify = .{ .writer = writer, .options = .{ .whitespace = .minified } }; + try s.write(value); + }, + } +} + +fn write_config_value_description(T: type, field_type: type, comptime field_name: []const u8, writer: *std.Io.Writer) !void { switch (@typeInfo(field_type)) { .int => switch (field_type) { u24 => try writer.print("24 bit hex color value in quotes", .{}), @@ -748,14 +778,17 @@ fn write_config_value_description(T: type, field_type: type, writer: *std.Io.Wri }, .optional => |info| switch (@typeInfo(info.child)) { else => { - try write_config_value_description(T, info.child, writer); + try write_config_value_description(T, info.child, field_name, writer); try writer.print(" or null", .{}); }, }, .pointer => |info| switch (info.size) { - .slice => if (info.child == u8) - try writer.print("quoted string", .{}) - else if (info.child == u16) + .slice => if (info.child == u8) { + if (std.mem.eql(u8, field_name, "include_files")) + try writer.print("quoted string of {c} separated paths", .{std.fs.path.delimiter}) + else + try writer.print("quoted string", .{}); + } else if (info.child == u16) try writer.print("quoted string (u16)", .{}) else if (info.child == []const u8) try writer.print("list of quoted strings", .{}) @@ -817,19 +850,6 @@ fn config_eql(config_type: type, T: type, a: T, b: T) bool { unsupported_error(config_type, T); } -fn write_json_file(comptime T: type, data: T, allocator: std.mem.Allocator, file_name: []const u8) !void { - var file = try std.fs.createFileAbsolute(file_name, .{ .truncate = true }); - defer file.close(); - - var cb = std.ArrayList(u8).init(allocator); - defer cb.deinit(); - try cbor.writeValue(cb.writer(), data); - - var s = std.json.writeStream(file.writer(), .{ .whitespace = .indent_4 }); - var iter: []const u8 = cb.items; - try cbor.JsonStream(std.fs.File).jsonWriteValue(&s, &iter); -} - pub fn read_keybind_namespace(allocator: std.mem.Allocator, namespace_name: []const u8) ?[]const u8 { const file_name = get_keybind_namespace_file_name(namespace_name) catch return null; var file = std.fs.openFileAbsolute(file_name, .{ .mode = .read_only }) catch return null; @@ -1044,7 +1064,7 @@ fn get_app_state_dir(appname: []const u8) ![]const u8 { } fn get_app_config_file_name(appname: []const u8, comptime base_name: []const u8) ConfigDirError![]const u8 { - return get_app_config_dir_file_name(appname, base_name ++ ".json"); + return get_app_config_dir_file_name(appname, base_name); } fn get_app_config_dir_file_name(appname: []const u8, comptime config_file_name: []const u8) ConfigDirError![]const u8 { diff --git a/src/tui/mainview.zig b/src/tui/mainview.zig index 754d243..e237a9e 100644 --- a/src/tui/mainview.zig +++ b/src/tui/mainview.zig @@ -349,7 +349,7 @@ fn open_style_config(self: *Self, Style: type) command.Result { tui.reset_drag_context(); try self.create_editor(); try command.executeName("open_scratch_buffer", command.fmt(.{ - file_name[0 .. file_name.len - ".json".len], + file_name, conf.written(), "conf", })); @@ -640,13 +640,13 @@ const cmds = struct { pub fn open_config(_: *Self, _: Ctx) Result { const file_name = try root.get_config_file_name(@import("config")); - try tp.self_pid().send(.{ "cmd", "navigate", .{ .file = file_name[0 .. file_name.len - 5] } }); + try tp.self_pid().send(.{ "cmd", "navigate", .{ .file = file_name } }); } pub const open_config_meta: Meta = .{ .description = "Edit configuration" }; pub fn open_gui_config(_: *Self, _: Ctx) Result { const file_name = try root.get_config_file_name(@import("gui_config")); - try tp.self_pid().send(.{ "cmd", "navigate", .{ .file = file_name[0 .. file_name.len - ".json".len] } }); + try tp.self_pid().send(.{ "cmd", "navigate", .{ .file = file_name } }); } pub const open_gui_config_meta: Meta = .{ .description = "Edit gui configuration" };