diff --git a/build.zig b/build.zig index a08602a10..9f5253b0c 100644 --- a/build.zig +++ b/build.zig @@ -102,6 +102,12 @@ pub fn build(b: *std.Build) !void { "Name of the conformance app to run with 'run' option.", ); + config.documentation = b.option( + bool, + "documentation", + "Build generated documentation", + ) orelse true; + const emit_test_exe = b.option( bool, "emit-test-exe", @@ -414,6 +420,9 @@ pub fn build(b: *std.Build) !void { }); } + // Documenation + if (config.documentation) buildDocumentation(b, version); + // App (Linux) if (target.result.os.tag == .linux and config.app_runtime != .none) { // https://developer.gnome.org/documentation/guidelines/maintainer/integrating.html @@ -1148,6 +1157,91 @@ fn addHelp( } } +/// Generate documentation (manpages, etc.) from help strings +fn buildDocumentation( + b: *std.Build, + version: std.SemanticVersion, +) void { + const manpages = [_]struct { + name: []const u8, + section: []const u8, + }{ + .{ + .name = "ghostty", + .section = "1", + }, + .{ + .name = "ghostty", + .section = "5", + }, + }; + + inline for (manpages) |manpage| { + const generate_markdown = b.addExecutable(.{ + .name = "generate_" ++ manpage.name ++ "_" ++ manpage.section ++ "_markdown", + .root_source_file = .{ + .path = "src/generate_" ++ manpage.name ++ "_" ++ manpage.section ++ "_markdown.zig", + }, + .target = b.host, + }); + + const generate_markdown_step = b.addRunArtifact(generate_markdown); + + const markdown_output = generate_markdown_step.captureStdOut(); + + const generate_markdown_options = b.addOptions(); + generate_markdown_options.addOption(std.SemanticVersion, "version", version); + generate_markdown.root_module.addOptions("build_options", generate_markdown_options); + + addHelp(b, generate_markdown); + + b.getInstallStep().dependOn(&b.addInstallFile( + markdown_output, + "share/ghostty/doc/" ++ manpage.name ++ "." ++ manpage.section ++ ".md", + ).step); + + const generate_html = b.addSystemCommand(&.{"pandoc"}); + generate_html.step.dependOn(&generate_markdown_step.step); + generate_html.addArgs( + &.{ + "--standalone", + "--from", + "markdown", + "--to", + "html", + }, + ); + generate_html.addFileArg(markdown_output); + + const html_output = generate_html.captureStdOut(); + + b.getInstallStep().dependOn(&b.addInstallFile( + html_output, + "share/ghostty/doc/" ++ manpage.name ++ "." ++ manpage.section ++ ".html", + ).step); + + const generate_manpage = b.addSystemCommand(&.{"pandoc"}); + generate_manpage.step.dependOn(&generate_markdown_step.step); + generate_manpage.addArgs( + &.{ + "--standalone", + "--from", + "markdown", + "--to", + "man", + }, + ); + generate_manpage.addFileArg(markdown_output); + + const manpage_output = generate_manpage.captureStdOut(); + + b.getInstallStep().dependOn(&b.addInstallFile( + manpage_output, + "share/man/man" ++ manpage.section ++ "/" ++ manpage.name ++ "." ++ manpage.section, + ).step); + } +} + fn benchSteps( b: *std.Build, target: std.Build.ResolvedTarget, diff --git a/nix/devShell.nix b/nix/devShell.nix index 00418c0a8..782498b57 100644 --- a/nix/devShell.nix +++ b/nix/devShell.nix @@ -45,6 +45,7 @@ pixman, zlib, alejandra, + pandoc, }: let # See package.nix. Keep in sync. rpathLibs = @@ -80,6 +81,7 @@ in # For builds llvmPackages_latest.llvm ncurses + pandoc pkg-config scdoc zig diff --git a/nix/package.nix b/nix/package.nix index 3f3136553..c19221584 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -23,6 +23,7 @@ ncurses, pkg-config, zig_0_12, + pandoc, revision ? "dirty", }: let # The Zig hook has no way to select the release type without actual @@ -90,6 +91,7 @@ in nativeBuildInputs = [ git ncurses + pandoc pkg-config zig012Hook wrapGAppsHook4 diff --git a/src/build_config.zig b/src/build_config.zig index 6a9a55085..ae324a6d7 100644 --- a/src/build_config.zig +++ b/src/build_config.zig @@ -21,6 +21,7 @@ pub const BuildConfig = struct { static: bool = false, flatpak: bool = false, libadwaita: bool = false, + documentation: bool = true, app_runtime: apprt.Runtime = .none, renderer: rendererpkg.Impl = .opengl, font_backend: font.Backend = .freetype, diff --git a/src/doc/ghostty_1_footer.md b/src/doc/ghostty_1_footer.md new file mode 100644 index 000000000..97f36dada --- /dev/null +++ b/src/doc/ghostty_1_footer.md @@ -0,0 +1,40 @@ +# FILES + +_\$XDG_CONFIG_HOME/ghostty/config_ + +: Location of the default configuration file. + +_\$LOCALAPPDATA/ghostty/config_ + +: **On Windows**, if _\$XDG_CONFIG_HOME_ is not set, _\$LOCALAPPDATA_ will be searched +for configuration files. + +# ENVIRONMENT + +**TERM** + +: Defaults to `xterm-ghostty`. Can be configured with the `term` configuration option. + +**GHOSTTY_RESOURCES_DIR** + +: Where the Ghostty resources can be found. + +**XDG_CONFIG_HOME** + +: Default location for configuration files. + +**LOCALAPPDATA** + +: **WINDOWS ONLY:** alternate location to search for configuration files. + +# BUGS + +See GitHub issues: + +# AUTHOR + +Mitchell Hashimoto + +# SEE ALSO + +**ghostty(5)** diff --git a/src/doc/ghostty_1_header.md b/src/doc/ghostty_1_header.md new file mode 100644 index 000000000..c94ae68ca --- /dev/null +++ b/src/doc/ghostty_1_header.md @@ -0,0 +1,23 @@ +% GHOSTTY(1) Version @@VERSION@@ | Ghostty terminal emulator + +# NAME + +**ghostty** - Ghostty terminal emulator + +# DESCRIPTION + +Ghostty is a cross-platform, GPU-accelerated terminal emulator that aims to push +the boundaries of what is possible with a terminal emulator by exposing modern, +opt-in features that enable CLI tool developers to build more feature rich, +interactive applications. + +There are a number of excellent terminal emulator options that exist today. +The unique goal of Ghostty is to have a platform for experimenting with modern, +optional, non-standards-compliant features to enhance the capabilities of CLI +applications. We aim to be the best in this category, and competitive in the +rest. + +While aiming for this ambitious goal, Ghostty is a fully standards compliant +terminal emulator that aims to remain compatible with all existing shells and +software. You can use this as a drop-in replacement for your existing terminal +emulator. diff --git a/src/doc/ghostty_5_footer.md b/src/doc/ghostty_5_footer.md new file mode 100644 index 000000000..652b71d14 --- /dev/null +++ b/src/doc/ghostty_5_footer.md @@ -0,0 +1,32 @@ +# FILES + +_\$XDG_CONFIG_HOME/ghostty/config_ + +: Location of the default configuration file. + +_\$LOCALAPPDATA/ghostty/config_ + +: **On Windows**, if _\$XDG_CONFIG_HOME_ is not set, _\$LOCALAPPDATA_ will be searched +for configuration files. + +# ENVIRONMENT + +**XDG_CONFIG_HOME** + +: Default location for configuration files. + +**LOCALAPPDATA** + +: **WINDOWS ONLY:** alternate location to search for configuration files. + +# BUGS + +See GitHub issues: + +# AUTHOR + +Mitchell Hashimoto + +# SEE ALSO + +**ghostty(1)** diff --git a/src/doc/ghostty_5_header.md b/src/doc/ghostty_5_header.md new file mode 100644 index 000000000..bd724df53 --- /dev/null +++ b/src/doc/ghostty_5_header.md @@ -0,0 +1,90 @@ +% GHOSTTY(5) Version @@VERSION@@ | Ghostty terminal emulator configuration file + +# NAME + +**ghostty** - Ghostty terminal emulator configuration file + +# DESCRIPTION + +To configure Ghostty, you must use a configuration file. GUI-based configuration +is on the roadmap but not yet supported. The configuration file must be placed +at `$XDG_CONFIG_HOME/ghostty/config`, which defaults to `~/.config/ghostty/config` +if the [XDG environment is not set](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html). + +The file format is documented below as an example: + + # The syntax is "key = value". The whitespace around the equals doesn't matter. + background = 282c34 + foreground= ffffff + + # Blank lines are ignored! + + keybind = ctrl+z=close_surface + keybind = ctrl+d=new_split:right + + # Colors can be changed by setting the 16 colors of `palette`, which each color + # being defined as regular and bold. + # + # black + palette = 0=#1d2021 + palette = 8=#7c6f64 + # red + palette = 1=#cc241d + palette = 9=#fb4934 + # green + palette = 2=#98971a + palette = 10=#b8bb26 + # yellow + palette = 3=#d79921 + palette = 11=#fabd2f + # blue + palette = 4=#458588 + palette = 12=#83a598 + # purple + palette = 5=#b16286 + palette = 13=#d3869b + # aqua + palette = 6=#689d6a + palette = 14=#8ec07c + # white + palette = 7=#a89984 + palette = 15=#fbf1c7 + +You can view all available configuration options and their documentation by +executing the command `ghostty +show-config --default --docs`. Note that this will +output the full default configuration with docs to stdout, so you may want to +pipe that through a pager, an editor, etc. + +Note: You'll see a lot of weird blank configurations like `font-family =`. This +is a valid syntax to specify the default behavior (no value). The `+show-config` +outputs it so its clear that key is defaulting and also to have something to +attach the doc comment to. + +You can also see and read all available configuration options in the source +Config structure. The available keys are the keys verbatim, and their possible +values are typically documented in the comments. You also can search for +the public config files of many Ghostty users for examples and inspiration. + +## Configuration Errors + +If your configuration file has any errors, Ghostty does its best to ignore +them and move on. Configuration errors currently show up in the log. The log +is written directly to stderr, so it is up to you to figure out how to access +that for your system (for now). On macOS, you can also use the system `log` CLI +utility. See the Mac App section for more information. + +## Debugging Configuration + +You can verify that configuration is being properly loaded by looking at the +debug output of Ghostty. Documentation for how to view the debug output is in +the "building Ghostty" section at the end of the README. + +In the debug output, you should see in the first 20 lines or so messages about +loading (or not loading) a configuration file, as well as any errors it may have +encountered. Configuration errors are also shown in a dedicated window on both +macOS and Linux (GTK). Ghostty does not treat configuration errors as fatal and +will fall back to default values for erroneous keys. + +You can also view the full configuration Ghostty is loading using `ghostty ++show-config` from the command-line. Use the `--help` flag to additional options +for that command. diff --git a/src/generate_ghostty_1_markdown.zig b/src/generate_ghostty_1_markdown.zig new file mode 100644 index 000000000..bac2b8470 --- /dev/null +++ b/src/generate_ghostty_1_markdown.zig @@ -0,0 +1,16 @@ +const std = @import("std"); +const gen = @import("generate_markdown.zig"); + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + const alloc = gpa.allocator(); + + const writer = std.io.getStdOut().writer(); + + try gen.substitute(alloc, @embedFile("doc/ghostty_1_header.md"), writer); + + try gen.generate_actions(writer); + try gen.generate_config(writer, true); + + try gen.substitute(alloc, @embedFile("doc/ghostty_1_footer.md"), writer); +} diff --git a/src/generate_ghostty_5_markdown.zig b/src/generate_ghostty_5_markdown.zig new file mode 100644 index 000000000..31348daac --- /dev/null +++ b/src/generate_ghostty_5_markdown.zig @@ -0,0 +1,15 @@ +const std = @import("std"); +const gen = @import("generate_markdown.zig"); + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + const alloc = gpa.allocator(); + + const output = std.io.getStdOut().writer(); + + try gen.substitute(alloc, @embedFile("doc/ghostty_5_header.md"), output); + + try gen.generate_config(output, false); + + try gen.substitute(alloc, @embedFile("doc/ghostty_5_footer.md"), output); +} diff --git a/src/generate_markdown.zig b/src/generate_markdown.zig new file mode 100644 index 000000000..7ae40d566 --- /dev/null +++ b/src/generate_markdown.zig @@ -0,0 +1,86 @@ +const std = @import("std"); +const Config = @import("config/Config.zig"); +const Action = @import("cli/action.zig").Action; +const help_strings = @import("help_strings"); +const build_options = @import("build_options"); + +pub fn substitute(alloc: std.mem.Allocator, input: []const u8, writer: anytype) !void { + const version_string = try std.fmt.allocPrint(alloc, "{}", .{build_options.version}); + + const output = try alloc.alloc(u8, std.mem.replacementSize(u8, input, "@@VERSION@@", version_string)); + defer alloc.free(output); + _ = std.mem.replace(u8, input, "@@VERSION@@", version_string, output); + try writer.writeAll(output); +} + +pub fn generate_config(writer: anytype, cli: bool) !void { + try writer.writeAll( + \\ + \\# CONFIGURATION OPTIONS + \\ + \\ + ); + + const info = @typeInfo(Config); + std.debug.assert(info == .Struct); + + inline for (info.Struct.fields) |field| { + if (field.name[0] == '_') continue; + + try writer.writeAll("`"); + if (cli) try writer.writeAll("--"); + try writer.writeAll(field.name); + try writer.writeAll("`\n\n"); + if (@hasDecl(help_strings.Config, field.name)) { + var iter = std.mem.splitScalar(u8, @field(help_strings.Config, field.name), '\n'); + var first = true; + while (iter.next()) |s| { + try writer.writeAll(if (first) ": " else " "); + try writer.writeAll(s); + try writer.writeAll("\n"); + first = false; + } + try writer.writeAll("\n\n"); + } + } +} + +pub fn generate_actions(writer: anytype) !void { + try writer.writeAll( + \\ + \\# COMMAND LINE ACTIONS + \\ + \\ + ); + + const info = @typeInfo(Action); + std.debug.assert(info == .Enum); + + inline for (info.Enum.fields) |field| { + if (field.name[0] == '_') continue; + + const action = std.meta.stringToEnum(Action, field.name).?; + + switch (action) { + .help => try writer.writeAll("`--help`\n\n"), + .version => try writer.writeAll("`--version`\n\n"), + else => { + try writer.writeAll("`+"); + try writer.writeAll(field.name); + try writer.writeAll("`\n\n"); + }, + } + + if (@hasDecl(help_strings.Action, field.name)) { + var iter = std.mem.splitScalar(u8, @field(help_strings.Action, field.name), '\n'); + var first = true; + while (iter.next()) |s| { + try writer.writeAll(if (first) ": " else " "); + try writer.writeAll(s); + try writer.writeAll("\n"); + first = false; + } + try writer.writeAll("\n\n"); + } + } +}