build(komorebic-core): add the crate with the docgen renderer

A new workspace member crates/komorebic-core, the home of the clap definitions the three komorebic packages share (step 4, D20-D22). This revision only has the docgen renderer: DocFormat keeps what differs between the trees (KDE pins the width at 100, fences with ```text, ends with a newline and writes a page for a subcommand of a subcommand; Windows and macOS set no width) and the list of commands that get no page, which is the same in all three. Nothing uses it yet.

Verified: native cargo test -p komorebic-core: 3 passed, 0 failed.
This commit is contained in:
LGUG2Z committed 2026-10-31 01:37:54 -07:00
1 parent 5cc38da61d
commit e09f3591bd
5 files changed
+189

No files matched your search

Generated
+7
View File
@@ -3571,6 +3571,13 @@ dependencies = [
"windows 0.62.2",
]
[[package]]
name = "komorebic-core"
version = "0.1.42"
dependencies = [
"clap",
]
[[package]]
name = "komorebic-no-console"
version = "0.1.42"
+1
View File
@@ -10,6 +10,7 @@ members = [
"komorebi-scrolling",
"komorebic",
"komorebic-no-console",
"crates/komorebic-core",
"komorebi-bar",
"komorebi-themes",
"crates/komorebi-protocol",
+8
View File
@@ -0,0 +1,8 @@
[package]
name = "komorebic-core"
version = "0.1.42"
description = "The command line definitions that the komorebic of every platform shares"
edition = "2024"
[dependencies]
clap = { workspace = true }
+165
View File
@@ -0,0 +1,165 @@
//! The pages of `docs/cli`: the long help of every subcommand, one Markdown file each.
use clap::Command;
use std::io;
use std::path::Path;
use std::path::PathBuf;
/// The subcommands that get no page: the generator itself, and the ones that are hidden or
/// platform plumbing that is not documented.
pub const IGNORED: [&str; 12] = [
"docgen",
"splash",
"alt-focus-hack",
"identify-border-overflow-application",
"load-custom-layout",
"workspace-custom-layout",
"named-workspace-custom-layout",
"workspace-custom-layout-rule",
"named-workspace-custom-layout-rule",
"focus-follows-mouse",
"toggle-focus-follows-mouse",
"format-app-specific-configuration",
];
/// How a platform's pages look. Each tree keeps the format its checked-in pages have.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct DocFormat {
/// Wrap the help at this many columns whatever the width of the terminal, or at the width of
/// the terminal if there is none.
pub term_width: Option<usize>,
/// What the opening fence of a page says (`text`), or nothing.
pub fence_info: &'static str,
/// Whether a page ends with a newline after the closing fence.
pub trailing_newline: bool,
/// Whether a subcommand of a subcommand (`hotkeys compile`) gets a page of its own, named
/// `hotkeys-compile`.
pub nested: bool,
}
impl DocFormat {
/// The pages of komorebic for Windows and for macOS.
pub const PLAIN: Self = Self {
term_width: None,
fence_info: "",
trailing_newline: false,
nested: false,
};
/// The pages of komorebic for KDE: the same whatever the width of the terminal.
pub const KDE: Self = Self {
term_width: Some(100),
fence_info: "text",
trailing_newline: true,
nested: true,
};
fn render(&self, cmd: Command) -> String {
let mut cmd = match self.term_width {
Some(width) => cmd.term_width(width),
None => cmd,
};
let help_text = cmd.render_long_help().to_string();
let newline = if self.trailing_newline { "\n" } else { "" };
format!("```{}\n{help_text}\n```{newline}", self.fence_info)
}
}
/// The pages of a command line, as `(file name, content)` in the order of its subcommands.
pub fn pages(mut cli: Command, format: &DocFormat) -> Vec<(String, String)> {
let mut pages = Vec::new();
for cmd in cli.get_subcommands_mut() {
let name = cmd.get_name().to_string();
if IGNORED.contains(&name.as_str()) {
continue;
}
pages.push((format!("{name}.md"), format.render(cmd.clone())));
if format.nested {
for sub in cmd.get_subcommands() {
pages.push((
format!("{name}-{}.md", sub.get_name()),
format.render(sub.clone()),
));
}
}
}
pages
}
/// Write the pages into `dir` (made if it is not there) and return the paths written.
pub fn write(cli: Command, format: &DocFormat, dir: &Path) -> io::Result<Vec<PathBuf>> {
std::fs::create_dir_all(dir)?;
pages(cli, format)
.into_iter()
.map(|(file, content)| {
let path = dir.join(file);
std::fs::write(&path, content)?;
Ok(path)
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use clap::CommandFactory;
use clap::Parser;
use clap::Subcommand;
#[derive(Parser)]
struct Cli {
#[command(subcommand)]
cmd: Cmd,
}
#[derive(Subcommand)]
enum Cmd {
/// Do the thing
Thing,
/// Group of things
Group {
#[command(subcommand)]
sub: Sub,
},
#[command(hide = true)]
Docgen,
}
#[derive(Subcommand)]
enum Sub {
/// Do one of the things
One,
}
#[test]
fn a_plain_page_has_a_bare_fence_and_no_final_newline() {
let pages = pages(Cli::command(), &DocFormat::PLAIN);
let names: Vec<_> = pages.iter().map(|(name, _)| name.as_str()).collect();
assert_eq!(names, ["thing.md", "group.md"]);
assert!(pages[0].1.starts_with("```\nDo the thing\n"));
assert!(pages[0].1.ends_with("\n```"));
}
#[test]
fn a_kde_page_says_text_ends_in_a_newline_and_nests() {
let pages = pages(Cli::command(), &DocFormat::KDE);
let names: Vec<_> = pages.iter().map(|(name, _)| name.as_str()).collect();
assert_eq!(names, ["thing.md", "group.md", "group-one.md"]);
assert!(pages[0].1.starts_with("```text\nDo the thing\n"));
assert!(pages[0].1.ends_with("\n```\n"));
}
#[test]
fn write_returns_the_paths_it_wrote() {
let dir =
std::env::temp_dir().join(format!("komorebic-core-docgen-{}", std::process::id()));
let written = write(Cli::command(), &DocFormat::PLAIN, &dir).unwrap();
assert_eq!(written.len(), 2);
assert!(
std::fs::read_to_string(dir.join("thing.md"))
.unwrap()
.contains("Do the thing")
);
std::fs::remove_dir_all(&dir).unwrap();
}
}
+8
View File
@@ -0,0 +1,8 @@
//! The command line of `komorebic`, in one place for the three platforms.
//!
//! Every definition is plain clap code that is always compiled, with no `cfg`: what differs per
//! platform is a type parameter, so one binary can render the help of all of them.
//!
//! - [`docgen`] renders the pages of `docs/cli`.
pub mod docgen;