OCaml HTML5 parser/serialiser based on Python's JustHTML

Store poe config under unified XDG app name

Add xdg_app parameter to zulip-bot Config to allow custom XDG paths.
Poe now stores all configuration under ~/.config/poe/ including:
- config.toml (poe settings)
- zulip.config (zulip credentials)

Also renamed config file from "config" to "zulip.config" for clarity.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+53 -35
+20 -10
ocaml-zulip/lib/zulip_bot/config.ml
··· 14 14 api_key : string; 15 15 description : string option; 16 16 usage : string option; 17 + xdg_app : string option; 17 18 } 18 19 19 - let create ~name ~site ~email ~api_key ?description ?usage () = 20 - { name; site; email; api_key; description; usage } 20 + let create ~name ~site ~email ~api_key ?description ?usage ?xdg_app () = 21 + { name; site; email; api_key; description; usage; xdg_app } 21 22 22 23 (** Convert bot name to environment variable prefix. "my-bot" -> "ZULIP_MY_BOT" 23 24 *) ··· 67 68 |> defaults ini_section_codec ~enc:Fun.id 68 69 |> skip_unknown |> finish) 69 70 70 - let load ~fs name = 71 + let load ?xdg_app ~fs name = 71 72 Log.info (fun m -> m "Loading config for bot: %s" name); 72 - let xdg = Xdge.create fs ("zulip-bot/" ^ name) in 73 - let config_file = Eio.Path.(Xdge.config_dir xdg / "config") in 73 + let app = match xdg_app with Some app -> app | None -> "zulip-bot/" ^ name in 74 + let xdg = Xdge.create fs app in 75 + let config_file = Eio.Path.(Xdge.config_dir xdg / "zulip.config") in 74 76 Log.debug (fun m -> m "Looking for config at: %a" Eio.Path.pp config_file); 75 77 (* Try parsing with [bot] section first, fall back to bare config *) 76 78 let ini_config = ··· 89 91 api_key = ini_config.ini_api_key; 90 92 description = ini_config.ini_description; 91 93 usage = ini_config.ini_usage; 94 + xdg_app; 92 95 } 93 96 94 - let from_env name = 97 + let from_env ?xdg_app name = 95 98 Log.info (fun m -> m "Loading config for bot %s from environment" name); 96 99 let prefix = env_prefix name in 97 100 let get_env key = Sys.getenv_opt (prefix ^ key) in ··· 110 113 api_key = get_required "API_KEY"; 111 114 description = get_env "DESCRIPTION"; 112 115 usage = get_env "USAGE"; 116 + xdg_app; 113 117 } 114 118 115 - let load_or_env ~fs name = 116 - try load ~fs name 119 + let load_or_env ?xdg_app ~fs name = 120 + try load ?xdg_app ~fs name 117 121 with _ -> 118 122 Log.debug (fun m -> 119 123 m "Config file not found, falling back to environment variables"); 120 - from_env name 124 + from_env ?xdg_app name 121 125 122 - let xdg ~fs config = Xdge.create fs ("zulip-bot/" ^ config.name) 126 + let xdg ~fs config = 127 + let app = 128 + match config.xdg_app with 129 + | Some app -> app 130 + | None -> "zulip-bot/" ^ config.name 131 + in 132 + Xdge.create fs app 123 133 let data_dir ~fs config = Xdge.data_dir (xdg ~fs config) 124 134 let state_dir ~fs config = Xdge.state_dir (xdg ~fs config) 125 135 let cache_dir ~fs config = Xdge.cache_dir (xdg ~fs config)
+29 -21
ocaml-zulip/lib/zulip_bot/config.mli
··· 5 5 6 6 (** Bot configuration with XDG Base Directory support. 7 7 8 - Configuration is loaded from XDG-compliant locations using the bot's name to 9 - locate the appropriate configuration file. The configuration file should be 10 - in INI format with the following structure: 8 + Configuration is loaded from XDG-compliant locations. The configuration 9 + file should be in INI format with the following structure: 11 10 12 11 {v 13 12 [bot] ··· 21 20 v} 22 21 23 22 Configuration files are searched in XDG config directories: 24 - - [$XDG_CONFIG_HOME/zulip-bot/<name>/config] (typically 25 - [~/.config/zulip-bot/<name>/config]) 26 - - System directories as fallback 23 + - Default: [$XDG_CONFIG_HOME/zulip-bot/<name>/zulip.config] 24 + - With [~xdg_app:"myapp"]: [$XDG_CONFIG_HOME/myapp/zulip.config] 27 25 28 26 Environment variables can override file configuration: 29 27 - [ZULIP_<NAME>_SITE], [ZULIP_<NAME>_EMAIL], [ZULIP_<NAME>_API_KEY] ··· 38 36 api_key : string; (** Bot API key *) 39 37 description : string option; (** Optional bot description *) 40 38 usage : string option; (** Optional usage help text *) 39 + xdg_app : string option; (** Custom XDG app name for config/data paths *) 41 40 } 42 41 (** Bot configuration record. *) 43 42 ··· 48 47 api_key:string -> 49 48 ?description:string -> 50 49 ?usage:string -> 50 + ?xdg_app:string -> 51 51 unit -> 52 52 t 53 - (** [create ~name ~site ~email ~api_key ?description ?usage ()] creates a 54 - configuration programmatically. *) 53 + (** [create ~name ~site ~email ~api_key ?description ?usage ?xdg_app ()] 54 + creates a configuration programmatically. If [xdg_app] is provided, 55 + XDG directories will use that app name instead of [zulip-bot/<name>]. *) 55 56 56 - val load : fs:Eio.Fs.dir_ty Eio.Path.t -> string -> t 57 - (** [load ~fs name] loads configuration for a named bot from XDG config 58 - directory. 57 + val load : ?xdg_app:string -> fs:Eio.Fs.dir_ty Eio.Path.t -> string -> t 58 + (** [load ?xdg_app ~fs name] loads configuration for a named bot from XDG 59 + config directory. 59 60 60 61 Searches for configuration in: 61 - - [$XDG_CONFIG_HOME/zulip-bot/<name>/config] 62 - - System config directories as fallback 62 + - Default: [$XDG_CONFIG_HOME/zulip-bot/<name>/zulip.config] 63 + - With [~xdg_app:"myapp"]: [$XDG_CONFIG_HOME/myapp/zulip.config] 63 64 65 + @param xdg_app Custom XDG app name (overrides default [zulip-bot/<name>]) 64 66 @param fs The Eio filesystem 65 67 @param name The bot name 66 68 @raise Eio.Io if configuration file cannot be read or parsed 67 69 @raise Failure if required fields are missing *) 68 70 69 - val from_env : string -> t 70 - (** [from_env name] loads configuration from environment variables. 71 + val from_env : ?xdg_app:string -> string -> t 72 + (** [from_env ?xdg_app name] loads configuration from environment variables. 71 73 72 74 Reads the following environment variables (where [NAME] is the uppercase bot 73 75 name with hyphens replaced by underscores): ··· 77 79 - [ZULIP_<NAME>_DESCRIPTION] (optional) 78 80 - [ZULIP_<NAME>_USAGE] (optional) 79 81 82 + @param xdg_app Custom XDG app name for data/state/cache directories 80 83 @raise Failure if required environment variables are not set *) 81 84 82 - val load_or_env : fs:Eio.Fs.dir_ty Eio.Path.t -> string -> t 83 - (** [load_or_env ~fs name] loads config from XDG location, falling back to 84 - environment. 85 + val load_or_env : ?xdg_app:string -> fs:Eio.Fs.dir_ty Eio.Path.t -> string -> t 86 + (** [load_or_env ?xdg_app ~fs name] loads config from XDG location, falling 87 + back to environment. 85 88 86 89 Attempts to load from the XDG config file first. If that fails (file not 87 90 found or unreadable), falls back to environment variables. 88 91 92 + @param xdg_app Custom XDG app name (overrides default [zulip-bot/<name>]) 89 93 @raise Failure if neither source provides valid configuration *) 90 94 91 95 val xdg : fs:Eio.Fs.dir_ty Eio.Path.t -> t -> Xdge.t 92 96 (** [xdg ~fs config] returns the XDG context for this bot. 93 97 98 + Uses the [xdg_app] field if set, otherwise defaults to [zulip-bot/<name>]. 94 99 Useful for accessing data and state directories for the bot: 95 100 - [Xdge.data_dir (xdg ~fs config)] for persistent data 96 101 - [Xdge.state_dir (xdg ~fs config)] for runtime state ··· 99 104 val data_dir : fs:Eio.Fs.dir_ty Eio.Path.t -> t -> Eio.Fs.dir_ty Eio.Path.t 100 105 (** [data_dir ~fs config] returns the XDG data directory for this bot. 101 106 102 - Returns [$XDG_DATA_HOME/zulip-bot/<name>], creating it if necessary. *) 107 + Returns [$XDG_DATA_HOME/<app>] where [<app>] is either [config.xdg_app] 108 + or [zulip-bot/<name>]. Creates the directory if necessary. *) 103 109 104 110 val state_dir : fs:Eio.Fs.dir_ty Eio.Path.t -> t -> Eio.Fs.dir_ty Eio.Path.t 105 111 (** [state_dir ~fs config] returns the XDG state directory for this bot. 106 112 107 - Returns [$XDG_STATE_HOME/zulip-bot/<name>], creating it if necessary. *) 113 + Returns [$XDG_STATE_HOME/<app>] where [<app>] is either [config.xdg_app] 114 + or [zulip-bot/<name>]. Creates the directory if necessary. *) 108 115 109 116 val cache_dir : fs:Eio.Fs.dir_ty Eio.Path.t -> t -> Eio.Fs.dir_ty Eio.Path.t 110 117 (** [cache_dir ~fs config] returns the XDG cache directory for this bot. 111 118 112 - Returns [$XDG_CACHE_HOME/zulip-bot/<name>], creating it if necessary. *) 119 + Returns [$XDG_CACHE_HOME/<app>] where [<app>] is either [config.xdg_app] 120 + or [zulip-bot/<name>]. Creates the directory if necessary. *)
+4 -4
poe/bin/main.ml
··· 28 28 | None -> Poe.Config.default)) 29 29 in 30 30 31 - (* Load zulip bot config *) 32 - let zulip_config = Zulip_bot.Config.load_or_env ~fs bot_name in 31 + (* Load zulip bot config from poe's XDG directory *) 32 + let zulip_config = Zulip_bot.Config.load_or_env ~xdg_app:"poe" ~fs bot_name in 33 33 34 34 (* Create handler environment *) 35 35 let handler_env : _ Poe.Handler.env = ··· 105 105 | None -> Poe.Config.default)) 106 106 in 107 107 108 - let zulip_config = Zulip_bot.Config.load_or_env ~fs bot_name in 108 + let zulip_config = Zulip_bot.Config.load_or_env ~xdg_app:"poe" ~fs bot_name in 109 109 let client = Zulip_bot.Bot.create_client ~sw ~env ~config:zulip_config in 110 110 111 111 match Poe.Handler.read_changes_file ~fs poe_config with ··· 158 158 monorepo_path = \".\""; 159 159 `P 160 160 "Zulip credentials are loaded from \ 161 - $(b,~/.config/zulip-bot/<name>/config) or environment variables."; 161 + $(b,~/.config/poe/zulip.config) or environment variables."; 162 162 ] 163 163 in 164 164 Cmd.group info [ run_cmd; broadcast_cmd ]