chore: cleanup main.rs

This commit is contained in:
Matias Fontanini 2025-01-25 06:33:27 -08:00
parent efc833543f
commit 4f4b3684fe
2 changed files with 226 additions and 165 deletions

View File

@ -37,7 +37,7 @@ tl = "0.7"
thiserror = "2"
unicode-width = "0.2"
os_pipe = "1.1.5"
libc = "0.2.155"
libc = "0.2"
[dependencies.syntect]
version = "5.2"

View File

@ -137,184 +137,164 @@ struct Customizations {
code_executor: SnippetExecutor,
}
fn load_customizations(
config_file_path: Option<PathBuf>,
cwd: &Path,
) -> Result<Customizations, Box<dyn std::error::Error>> {
let configs_path: PathBuf = match env::var("XDG_CONFIG_HOME") {
Ok(path) => Path::new(&path).join("presenterm"),
Err(_) => {
let Some(project_dirs) = ProjectDirs::from("", "", "presenterm") else {
return Ok(Default::default());
};
project_dirs.config_dir().into()
}
};
let themes = load_themes(&configs_path)?;
let config_file_path = config_file_path.unwrap_or_else(|| configs_path.join("config.yaml"));
let config = Config::load(&config_file_path)?;
let code_executor = SnippetExecutor::new(config.snippet.exec.custom.clone(), cwd.to_path_buf())?;
Ok(Customizations { config, themes, code_executor })
}
impl Customizations {
fn load(config_file_path: Option<PathBuf>, cwd: &Path) -> Result<Self, Box<dyn std::error::Error>> {
let configs_path: PathBuf = match env::var("XDG_CONFIG_HOME") {
Ok(path) => Path::new(&path).join("presenterm"),
Err(_) => {
let Some(project_dirs) = ProjectDirs::from("", "", "presenterm") else {
return Ok(Default::default());
};
project_dirs.config_dir().into()
}
};
let themes = Self::load_themes(&configs_path)?;
let config_file_path = config_file_path.unwrap_or_else(|| configs_path.join("config.yaml"));
let config = Config::load(&config_file_path)?;
let code_executor = SnippetExecutor::new(config.snippet.exec.custom.clone(), cwd.to_path_buf())?;
Ok(Customizations { config, themes, code_executor })
}
fn load_themes(config_path: &Path) -> Result<Themes, Box<dyn std::error::Error>> {
let themes_path = config_path.join("themes");
fn load_themes(config_path: &Path) -> Result<Themes, Box<dyn std::error::Error>> {
let themes_path = config_path.join("themes");
let mut highlight_themes = HighlightThemeSet::default();
highlight_themes.register_from_directory(themes_path.join("highlighting"))?;
let mut highlight_themes = HighlightThemeSet::default();
highlight_themes.register_from_directory(themes_path.join("highlighting"))?;
let mut presentation_themes = PresentationThemeSet::default();
presentation_themes.register_from_directory(&themes_path)?;
let mut presentation_themes = PresentationThemeSet::default();
presentation_themes.register_from_directory(&themes_path)?;
let themes = Themes { presentation: presentation_themes, highlight: highlight_themes };
Ok(themes)
}
fn display_acknowledgements() {
let acknowledgements = include_bytes!("../bat/acknowledgements.txt");
println!("{}", String::from_utf8_lossy(acknowledgements));
}
fn make_builder_options(
config: &Config,
mode: &PresentMode,
force_default_theme: bool,
render_speaker_notes_only: bool,
) -> PresentationBuilderOptions {
PresentationBuilderOptions {
allow_mutations: !matches!(mode, PresentMode::Export),
implicit_slide_ends: config.options.implicit_slide_ends.unwrap_or_default(),
command_prefix: config.options.command_prefix.clone().unwrap_or_default(),
image_attribute_prefix: config.options.image_attributes_prefix.clone().unwrap_or_else(|| "image:".to_string()),
incremental_lists: config.options.incremental_lists.unwrap_or_default(),
force_default_theme,
end_slide_shorthand: config.options.end_slide_shorthand.unwrap_or_default(),
print_modal_background: false,
strict_front_matter_parsing: config.options.strict_front_matter_parsing.unwrap_or(true),
enable_snippet_execution: config.snippet.exec.enable,
enable_snippet_execution_replace: config.snippet.exec_replace.enable,
render_speaker_notes_only,
auto_render_languages: config.options.auto_render_languages.clone(),
let themes = Themes { presentation: presentation_themes, highlight: highlight_themes };
Ok(themes)
}
}
fn load_default_theme(config: &Config, themes: &Themes, cli: &Cli) -> PresentationTheme {
let default_theme_name =
cli.theme.as_ref().or(config.defaults.theme.as_ref()).map(|s| s.as_str()).unwrap_or(DEFAULT_THEME);
let Some(default_theme) = themes.presentation.load_by_name(default_theme_name) else {
let valid_themes = themes.presentation.theme_names().join(", ");
let error_message = format!("invalid theme name, valid themes are: {valid_themes}");
Cli::command().error(ErrorKind::InvalidValue, error_message).exit();
};
default_theme
struct CoreComponents {
third_party: ThirdPartyRender,
code_executor: Rc<SnippetExecutor>,
resources: Resources,
printer: Arc<ImagePrinter>,
builder_options: PresentationBuilderOptions,
themes: Themes,
default_theme: PresentationTheme,
config: Config,
present_mode: PresentMode,
graphics_mode: GraphicsMode,
}
fn select_graphics_mode(cli: &Cli, config: &Config) -> GraphicsMode {
if cli.enable_export_mode || cli.export_pdf || cli.generate_pdf_metadata {
GraphicsMode::AsciiBlocks
} else {
let protocol = cli.image_protocol.as_ref().unwrap_or(&config.defaults.image_protocol);
match GraphicsMode::try_from(protocol) {
Ok(mode) => mode,
Err(_) => {
Cli::command().error(ErrorKind::InvalidValue, "sixel support was not enabled during compilation").exit()
impl CoreComponents {
fn new(cli: &Cli, path: &Path) -> Result<Self, Box<dyn std::error::Error>> {
let mut resources_path = path.parent().unwrap_or(Path::new("./")).to_path_buf();
if resources_path == Path::new("") {
resources_path = "./".into();
}
let resources_path = resources_path.canonicalize().unwrap_or(resources_path);
let Customizations { config, themes, code_executor } =
Customizations::load(cli.config_file.clone().map(PathBuf::from), &resources_path)?;
let default_theme = Self::load_default_theme(&config, &themes, cli);
let force_default_theme = cli.theme.is_some();
let present_mode = match (cli.present, cli.enable_export_mode) {
(true, _) => PresentMode::Presentation,
(false, true) => PresentMode::Export,
(false, false) => PresentMode::Development,
};
let mut builder_options =
Self::make_builder_options(&config, &present_mode, force_default_theme, cli.listen_speaker_notes);
if cli.enable_snippet_execution {
builder_options.enable_snippet_execution = true;
}
if cli.enable_snippet_execution_replace {
builder_options.enable_snippet_execution_replace = true;
}
let graphics_mode = Self::select_graphics_mode(cli, &config);
let printer = Arc::new(ImagePrinter::new(graphics_mode.clone())?);
let registry = ImageRegistry(printer.clone());
let resources = Resources::new(resources_path.clone(), registry.clone());
let third_party_config = ThirdPartyConfigs {
typst_ppi: config.typst.ppi.to_string(),
mermaid_scale: config.mermaid.scale.to_string(),
threads: config.snippet.render.threads,
};
let third_party = ThirdPartyRender::new(third_party_config, registry, &resources_path);
let code_executor = Rc::new(code_executor);
Ok(Self {
third_party,
code_executor,
resources,
printer,
builder_options,
themes,
default_theme,
config,
present_mode,
graphics_mode,
})
}
fn make_builder_options(
config: &Config,
mode: &PresentMode,
force_default_theme: bool,
render_speaker_notes_only: bool,
) -> PresentationBuilderOptions {
PresentationBuilderOptions {
allow_mutations: !matches!(mode, PresentMode::Export),
implicit_slide_ends: config.options.implicit_slide_ends.unwrap_or_default(),
command_prefix: config.options.command_prefix.clone().unwrap_or_default(),
image_attribute_prefix: config
.options
.image_attributes_prefix
.clone()
.unwrap_or_else(|| "image:".to_string()),
incremental_lists: config.options.incremental_lists.unwrap_or_default(),
force_default_theme,
end_slide_shorthand: config.options.end_slide_shorthand.unwrap_or_default(),
print_modal_background: false,
strict_front_matter_parsing: config.options.strict_front_matter_parsing.unwrap_or(true),
enable_snippet_execution: config.snippet.exec.enable,
enable_snippet_execution_replace: config.snippet.exec_replace.enable,
render_speaker_notes_only,
auto_render_languages: config.options.auto_render_languages.clone(),
}
}
fn select_graphics_mode(cli: &Cli, config: &Config) -> GraphicsMode {
if cli.enable_export_mode || cli.export_pdf || cli.generate_pdf_metadata {
GraphicsMode::AsciiBlocks
} else {
let protocol = cli.image_protocol.as_ref().unwrap_or(&config.defaults.image_protocol);
match GraphicsMode::try_from(protocol) {
Ok(mode) => mode,
Err(_) => Cli::command()
.error(ErrorKind::InvalidValue, "sixel support was not enabled during compilation")
.exit(),
}
}
}
}
fn overflow_validation(mode: &PresentMode, config: &ValidateOverflows) -> bool {
match (config, mode) {
(ValidateOverflows::Always, _) => true,
(ValidateOverflows::Never, _) => false,
(ValidateOverflows::WhenPresenting, PresentMode::Presentation) => true,
(ValidateOverflows::WhenDeveloping, PresentMode::Development) => true,
_ => false,
fn load_default_theme(config: &Config, themes: &Themes, cli: &Cli) -> PresentationTheme {
let default_theme_name =
cli.theme.as_ref().or(config.defaults.theme.as_ref()).map(|s| s.as_str()).unwrap_or(DEFAULT_THEME);
let Some(default_theme) = themes.presentation.load_by_name(default_theme_name) else {
let valid_themes = themes.presentation.theme_names().join(", ");
let error_message = format!("invalid theme name, valid themes are: {valid_themes}");
Cli::command().error(ErrorKind::InvalidValue, error_message).exit();
};
default_theme
}
}
fn run(mut cli: Cli) -> Result<(), Box<dyn std::error::Error>> {
if cli.generate_config_file_schema {
let schema = schemars::schema_for!(Config);
serde_json::to_writer_pretty(io::stdout(), &schema).map_err(|e| format!("failed to write schema: {e}"))?;
return Ok(());
} else if cli.acknowledgements {
display_acknowledgements();
return Ok(());
} else if cli.list_themes {
let Customizations { config, themes, .. } =
load_customizations(cli.config_file.clone().map(PathBuf::from), &current_dir()?)?;
let bindings = config.bindings.try_into()?;
let demo = ThemesDemo::new(themes, bindings, io::stdout())?;
demo.run()?;
return Ok(());
}
struct SpeakerNotesComponents {
events_listener: Option<SpeakerNotesEventListener>,
events_publisher: Option<SpeakerNotesEventPublisher>,
}
let Some(path) = cli.path.take() else {
Cli::command().error(ErrorKind::MissingRequiredArgument, "no path specified").exit();
};
let mut resources_path = path.parent().unwrap_or(Path::new("./")).to_path_buf();
if resources_path == Path::new("") {
resources_path = "./".into();
}
let resources_path = resources_path.canonicalize().unwrap_or(resources_path);
let Customizations { config, themes, code_executor } =
load_customizations(cli.config_file.clone().map(PathBuf::from), &resources_path)?;
let default_theme = load_default_theme(&config, &themes, &cli);
let force_default_theme = cli.theme.is_some();
let mode = match (cli.present, cli.enable_export_mode) {
(true, _) => PresentMode::Presentation,
(false, true) => PresentMode::Export,
(false, false) => PresentMode::Development,
};
let arena = Arena::new();
let parser = MarkdownParser::new(&arena);
let validate_overflows = overflow_validation(&mode, &config.defaults.validate_overflows) || cli.validate_overflows;
let mut options = make_builder_options(&config, &mode, force_default_theme, cli.listen_speaker_notes);
if cli.enable_snippet_execution {
options.enable_snippet_execution = true;
}
if cli.enable_snippet_execution_replace {
options.enable_snippet_execution_replace = true;
}
let graphics_mode = select_graphics_mode(&cli, &config);
let printer = Arc::new(ImagePrinter::new(graphics_mode.clone())?);
let registry = ImageRegistry(printer.clone());
let resources = Resources::new(resources_path.clone(), registry.clone());
let third_party_config = ThirdPartyConfigs {
typst_ppi: config.typst.ppi.to_string(),
mermaid_scale: config.mermaid.scale.to_string(),
threads: config.snippet.render.threads,
};
let third_party = ThirdPartyRender::new(third_party_config, registry, &resources_path);
let code_executor = Rc::new(code_executor);
if cli.export_pdf || cli.generate_pdf_metadata {
let mut exporter =
Exporter::new(parser, &default_theme, resources, third_party, code_executor, themes, options);
let mut args = Vec::new();
if let Some(theme) = cli.theme.as_ref() {
args.extend(["--theme", theme]);
}
if let Some(path) = cli.config_file.as_ref() {
args.extend(["--config-file", path]);
}
if cli.enable_snippet_execution {
args.push("-x");
}
if cli.enable_snippet_execution_replace {
args.push("-X");
}
if cli.export_pdf {
exporter.export_pdf(&path, &args)?;
} else {
let meta = exporter.generate_metadata(&path)?;
println!("{}", serde_json::to_string_pretty(&meta)?);
}
} else {
let full_presentation_path = path.canonicalize().unwrap_or_else(|_| path.clone());
impl SpeakerNotesComponents {
fn new(cli: &Cli, config: &Config, path: &Path) -> anyhow::Result<Self> {
let full_presentation_path = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
let publish_speaker_notes =
cli.publish_speaker_notes || (config.speaker_notes.always_publish && !cli.listen_speaker_notes);
let events_publisher = publish_speaker_notes
@ -328,12 +308,93 @@ fn run(mut cli: Cli) -> Result<(), Box<dyn std::error::Error>> {
.then(|| SpeakerNotesEventListener::new(config.speaker_notes.listen_address, full_presentation_path))
.transpose()
.map_err(|e| anyhow!("failed to create speaker notes listener: {e}"))?;
Ok(Self { events_listener, events_publisher })
}
}
fn overflow_validation_enabled(mode: &PresentMode, config: &ValidateOverflows) -> bool {
match (config, mode) {
(ValidateOverflows::Always, _) => true,
(ValidateOverflows::Never, _) => false,
(ValidateOverflows::WhenPresenting, PresentMode::Presentation) => true,
(ValidateOverflows::WhenDeveloping, PresentMode::Development) => true,
_ => false,
}
}
fn build_exporter_args(cli: &Cli) -> Vec<&str> {
let mut args = Vec::new();
if let Some(theme) = cli.theme.as_ref() {
args.extend(["--theme", theme]);
}
if let Some(path) = cli.config_file.as_ref() {
args.extend(["--config-file", path]);
}
if cli.enable_snippet_execution {
args.push("-x");
}
if cli.enable_snippet_execution_replace {
args.push("-X");
}
args
}
fn run(cli: Cli) -> Result<(), Box<dyn std::error::Error>> {
if cli.generate_config_file_schema {
let schema = schemars::schema_for!(Config);
serde_json::to_writer_pretty(io::stdout(), &schema).map_err(|e| format!("failed to write schema: {e}"))?;
return Ok(());
} else if cli.acknowledgements {
let acknowledgements = include_bytes!("../bat/acknowledgements.txt");
println!("{}", String::from_utf8_lossy(acknowledgements));
return Ok(());
} else if cli.list_themes {
let Customizations { config, themes, .. } =
Customizations::load(cli.config_file.clone().map(PathBuf::from), &current_dir()?)?;
let bindings = config.bindings.try_into()?;
let demo = ThemesDemo::new(themes, bindings, io::stdout())?;
demo.run()?;
return Ok(());
}
let Some(path) = cli.path.clone() else {
Cli::command().error(ErrorKind::MissingRequiredArgument, "no path specified").exit();
};
let CoreComponents {
third_party,
code_executor,
resources,
printer,
mut builder_options,
themes,
default_theme,
config,
present_mode,
graphics_mode,
} = CoreComponents::new(&cli, &path)?;
let arena = Arena::new();
let parser = MarkdownParser::new(&arena);
let validate_overflows =
overflow_validation_enabled(&present_mode, &config.defaults.validate_overflows) || cli.validate_overflows;
if cli.export_pdf || cli.generate_pdf_metadata {
let mut exporter =
Exporter::new(parser, &default_theme, resources, third_party, code_executor, themes, builder_options);
if cli.export_pdf {
let args = build_exporter_args(&cli);
exporter.export_pdf(&path, &args)?;
} else {
let meta = exporter.generate_metadata(&path)?;
println!("{}", serde_json::to_string_pretty(&meta)?);
}
} else {
let SpeakerNotesComponents { events_listener, events_publisher } =
SpeakerNotesComponents::new(&cli, &config, &path)?;
let command_listener = CommandListener::new(config.bindings.clone(), events_listener)?;
options.print_modal_background = matches!(graphics_mode, GraphicsMode::Kitty { .. });
builder_options.print_modal_background = matches!(graphics_mode, GraphicsMode::Kitty { .. });
let options = PresenterOptions {
builder_options: options,
mode,
builder_options,
mode: present_mode,
font_size_fallback: config.defaults.terminal_font_size,
bindings: config.bindings,
validate_overflows,