My agentic slop goes here. Not intended for anyone else!

more

Changed files
+165 -199
stack
+1 -1
stack/karakeepe/bin/dune
···
(executable
(name karakeepe_cli)
(public_name karakeepe)
-
(libraries karakeepe keyeio xdge eiocmd eio eio_main cmdliner ptime))
···
(executable
(name karakeepe_cli)
(public_name karakeepe)
+
(libraries karakeepe keyeio xdge eiocmd eio eio_main cmdliner ptime logs logs.fmt logs.cli))
+4 -7
stack/karakeepe/bin/karakeepe_cli.ml
···
|> Option.value ~default:"https://hoard.recoil.org"
in
-
Printf.printf "Fetching bookmarks from %s...\n" base_url;
-
try
let bookmarks = Karakeepe.fetch_all_bookmarks
~sw ~env ~api_key
-
~page_size:limit
~include_content:true
?filter_tags:(if tags = [] then None else Some tags)
base_url
···
1
end else begin
try
-
Printf.printf "Searching for bookmarks with tags: %s\n" (String.concat ", " tags);
-
let bookmarks = Karakeepe.fetch_all_bookmarks
~sw ~env ~api_key
-
~page_size:limit
~filter_tags:tags
~include_content:true
base_url
in
-
Printf.printf "Found %d bookmarks\n\n" (List.length bookmarks);
List.iteri (fun i (b : Karakeepe.bookmark) ->
Printf.printf "%d. %s\n" (i + 1) b.url;
···
|> Option.value ~default:"https://hoard.recoil.org"
in
try
let bookmarks = Karakeepe.fetch_all_bookmarks
~sw ~env ~api_key
+
~max_bookmarks:limit
~include_content:true
?filter_tags:(if tags = [] then None else Some tags)
base_url
···
1
end else begin
try
let bookmarks = Karakeepe.fetch_all_bookmarks
~sw ~env ~api_key
+
~max_bookmarks:limit
~filter_tags:tags
~include_content:true
base_url
in
+
Printf.printf "Found %d bookmark%s\n\n" (List.length bookmarks)
+
(if List.length bookmarks = 1 then "" else "s");
List.iteri (fun i (b : Karakeepe.bookmark) ->
Printf.printf "%d. %s\n" (i + 1) b.url;
+1 -1
stack/karakeepe/dune
···
(library
(name karakeepe)
(public_name karakeepe)
-
(libraries bushel eio eio.core requests ezjsonm fmt ptime uri))
···
(library
(name karakeepe)
(public_name karakeepe)
+
(libraries bushel eio eio.core requests ezjsonm fmt ptime uri logs logs.fmt))
+144 -109
stack/karakeepe/karakeepe.ml
···
module J = Ezjsonm
(** Type representing a Karakeep bookmark *)
type bookmark = {
id: string;
···
let id =
try J.find json ["id"] |> J.get_string
with e ->
-
prerr_endline (Fmt.str "Error parsing bookmark ID: %s" (Printexc.to_string e));
-
prerr_endline (Fmt.str "JSON: %s" (J.value_to_string json));
failwith "Unable to parse bookmark ID"
in
···
| Some (`String id) -> "karakeep-asset://" ^ id
| _ -> failwith "No URL or asset ID found in bookmark"))
| _ ->
-
prerr_endline (Fmt.str "Bookmark JSON structure: %s" (J.value_to_string json));
failwith "No URL found in bookmark"
in
···
{ id; title; url; note; created_at; updated_at; favourited; archived; tags;
tagging_status; summary; content; assets }
-
(** Parse a Karakeep bookmark response *)
let parse_bookmark_response json =
-
prerr_endline (Fmt.str "Full response JSON: %s" (J.value_to_string json));
-
try
let total = J.find json ["total"] |> J.get_int in
let bookmarks_json = J.find json ["data"] in
-
prerr_endline "Found bookmarks in data array";
let data = J.get_list parse_bookmark bookmarks_json in
let next_cursor =
try Some (J.find json ["nextCursor"] |> J.get_string)
with _ -> None
in
{ total; data; next_cursor }
-
with e1 ->
-
prerr_endline (Fmt.str "First format parse error: %s" (Printexc.to_string e1));
-
try
-
let bookmarks_json = J.find json ["bookmarks"] in
-
prerr_endline "Found bookmarks in bookmarks array";
-
let data =
-
try J.get_list parse_bookmark bookmarks_json
-
with e ->
-
prerr_endline (Fmt.str "Error parsing bookmarks array: %s" (Printexc.to_string e));
-
[]
-
in
-
let next_cursor =
-
try Some (J.find json ["nextCursor"] |> J.get_string)
-
with _ -> None
-
in
-
{ total = List.length data; data; next_cursor }
-
with e2 ->
-
prerr_endline (Fmt.str "Second format parse error: %s" (Printexc.to_string e2));
-
try
-
let error = J.find json ["error"] |> J.get_string in
-
let message =
-
try J.find json ["message"] |> J.get_string
-
with _ -> "Unknown error"
-
in
-
prerr_endline (Fmt.str "API Error: %s - %s" error message);
-
{ total = 0; data = []; next_cursor = None }
-
with _ ->
-
try
-
prerr_endline "Trying alternate array format";
-
prerr_endline (Fmt.str "JSON structure keys: %s"
(match json with
-
| `O fields -> String.concat ", " (List.map (fun (k, _) -> k) fields)
| _ -> "not an object"));
-
-
if J.find_opt json ["nextCursor"] <> None then begin
-
prerr_endline "Found nextCursor, checking alternate structures";
-
let bookmarks_json =
-
try Some (J.find json ["data"])
-
with _ -> None
-
in
-
match bookmarks_json with
-
| Some json_array ->
-
prerr_endline "Found bookmarks in data field";
-
begin try
-
let data = J.get_list parse_bookmark json_array in
-
let next_cursor =
-
try Some (J.find json ["nextCursor"] |> J.get_string)
-
with _ -> None
-
in
-
{ total = List.length data; data; next_cursor }
-
with e ->
-
prerr_endline (Fmt.str "Error parsing bookmarks from data: %s" (Printexc.to_string e));
-
{ total = 0; data = []; next_cursor = None }
-
end
-
| None ->
-
prerr_endline "No bookmarks found in alternate structure";
-
{ total = 0; data = []; next_cursor = None }
-
end
-
else begin
-
match json with
-
| `A _ ->
-
let data =
-
try J.get_list parse_bookmark json
-
with e ->
-
prerr_endline (Fmt.str "Error parsing root array: %s" (Printexc.to_string e));
-
[]
-
in
-
{ total = List.length data; data; next_cursor = None }
-
| _ ->
-
prerr_endline "Not an array at root level";
-
{ total = 0; data = []; next_cursor = None }
-
end
-
with e3 ->
-
prerr_endline (Fmt.str "Third format parse error: %s" (Printexc.to_string e3));
{ total = 0; data = []; next_cursor = None }
(** Fetch bookmarks from a Karakeep instance with pagination support *)
let fetch_bookmarks ~sw ~env ~api_key ?(limit=50) ?(offset=0) ?cursor ?(include_content=false) ?filter_tags base_url =
···
List.map (fun tag -> Uri.pct_encode ~component:`Query_key tag) tags
in
let tags_param = String.concat "," encoded_tags in
-
prerr_endline (Fmt.str "Adding tags filter: %s" tags_param);
url ^ "&tags=" ^ tags_param
| _ -> url
in
···
let headers = Requests.Headers.empty
|> Requests.Headers.set "Authorization" ("Bearer " ^ api_key) in
-
prerr_endline (Fmt.str "Fetching bookmarks from: %s" url);
try
let response = Requests.One.get ~sw ~clock:env#clock ~net:env#net ~headers url in
let status_code = Requests.Response.status_code response in
if status_code = 200 then begin
let body_str = Requests.Response.body response |> Eio.Flow.read_all in
-
prerr_endline (Fmt.str "Received %d bytes of response data" (String.length body_str));
try
let json = J.from_string body_str in
parse_bookmark_response json
with e ->
-
prerr_endline (Fmt.str "JSON parsing error: %s" (Printexc.to_string e));
-
prerr_endline (Fmt.str "Response body (first 200 chars): %s"
(if String.length body_str > 200 then String.sub body_str 0 200 ^ "..." else body_str));
raise e
end else begin
-
prerr_endline (Fmt.str "HTTP error %d" status_code);
failwith (Fmt.str "HTTP error: %d" status_code)
end
with e ->
-
prerr_endline (Fmt.str "Network error: %s" (Printexc.to_string e));
raise e
(** Fetch all bookmarks from a Karakeep instance using pagination *)
-
let fetch_all_bookmarks ~sw ~env ~api_key ?(page_size=50) ?max_pages ?filter_tags ?(include_content=false) base_url =
-
let rec fetch_pages page_num cursor acc _total_count =
-
let response =
-
match cursor with
-
| Some cursor_str -> fetch_bookmarks ~sw ~env ~api_key ~limit:page_size ~cursor:cursor_str ~include_content ?filter_tags base_url
-
| None -> fetch_bookmarks ~sw ~env ~api_key ~limit:page_size ~offset:(page_num * page_size) ~include_content ?filter_tags base_url
in
-
let all_bookmarks = acc @ response.data in
-
let more_available =
-
match response.next_cursor with
-
| Some _ -> true
-
| None ->
-
let fetched_count = (page_num * page_size) + List.length response.data in
-
fetched_count < response.total
-
in
-
let under_max_pages = match max_pages with
-
| None -> true
-
| Some max -> page_num + 1 < max
-
in
-
if more_available && under_max_pages then
-
fetch_pages (page_num + 1) response.next_cursor all_bookmarks response.total
-
else
-
all_bookmarks
in
-
fetch_pages 0 None [] 0
(** Fetch detailed information for a single bookmark by ID *)
let fetch_bookmark_details ~sw ~env ~api_key base_url bookmark_id =
···
module J = Ezjsonm
+
let src = Logs.Src.create "karakeepe" ~doc:"Karakeepe API client"
+
module Log = (val Logs.src_log src : Logs.LOG)
+
(** Type representing a Karakeep bookmark *)
type bookmark = {
id: string;
···
let id =
try J.find json ["id"] |> J.get_string
with e ->
+
Log.err (fun m -> m "Error parsing bookmark ID: %s@.JSON: %s"
+
(Printexc.to_string e) (J.value_to_string json));
failwith "Unable to parse bookmark ID"
in
···
| Some (`String id) -> "karakeep-asset://" ^ id
| _ -> failwith "No URL or asset ID found in bookmark"))
| _ ->
+
Log.err (fun m -> m "No URL found in bookmark@.JSON structure: %s"
+
(J.value_to_string json));
failwith "No URL found in bookmark"
in
···
{ id; title; url; note; created_at; updated_at; favourited; archived; tags;
tagging_status; summary; content; assets }
+
(** Parse a Karakeep bookmark response - handles multiple API response formats *)
let parse_bookmark_response json =
+
Log.debug (fun m -> m "Parsing API response: %s" (J.value_to_string json));
+
(* Try format 1: {total: int, data: [...], nextCursor?: string} *)
+
let try_format1 () =
+
Log.debug (fun m -> m "Trying format 1: {total, data, nextCursor}");
let total = J.find json ["total"] |> J.get_int in
let bookmarks_json = J.find json ["data"] in
let data = J.get_list parse_bookmark bookmarks_json in
let next_cursor =
try Some (J.find json ["nextCursor"] |> J.get_string)
with _ -> None
in
+
Log.debug (fun m -> m "Successfully parsed format 1: %d bookmarks" (List.length data));
{ total; data; next_cursor }
+
in
+
+
(* Try format 2: {bookmarks: [...], nextCursor?: string} - no total field *)
+
let try_format2 () =
+
Log.debug (fun m -> m "Trying format 2: {bookmarks, nextCursor}");
+
let bookmarks_json = J.find json ["bookmarks"] in
+
let data = J.get_list parse_bookmark bookmarks_json in
+
let next_cursor =
+
try Some (J.find json ["nextCursor"] |> J.get_string)
+
with _ -> None
+
in
+
(* Calculate total from data length when total field is missing *)
+
let total = List.length data in
+
Log.debug (fun m -> m "Successfully parsed format 2: %d bookmarks" total);
+
{ total; data; next_cursor }
+
in
+
+
(* Try format 3: API error response {error: string, message?: string} *)
+
let try_error_format () =
+
Log.debug (fun m -> m "Checking for API error response");
+
let error = J.find json ["error"] |> J.get_string in
+
let message =
+
try J.find json ["message"] |> J.get_string
+
with _ -> "Unknown error"
+
in
+
Log.err (fun m -> m "API returned error: %s - %s" error message);
+
{ total = 0; data = []; next_cursor = None }
+
in
+
+
(* Try format 4: Plain array at root level *)
+
let try_array_format () =
+
Log.debug (fun m -> m "Trying format 4: array at root");
+
match json with
+
| `A _ ->
+
let data = J.get_list parse_bookmark json in
+
Log.debug (fun m -> m "Successfully parsed array format: %d bookmarks" (List.length data));
+
{ total = List.length data; data; next_cursor = None }
+
| _ -> raise Not_found
+
in
+
+
(* Try each format in order *)
+
try try_format1 ()
+
with _ -> (
+
try try_format2 ()
+
with _ -> (
+
try try_error_format ()
+
with _ -> (
+
try try_array_format ()
+
with _ ->
+
Log.err (fun m -> m "Failed to parse response in any known format");
+
Log.debug (fun m -> m "JSON keys: %s"
(match json with
+
| `O fields -> String.concat ", " (List.map fst fields)
| _ -> "not an object"));
{ total = 0; data = []; next_cursor = None }
+
)
+
)
+
)
(** Fetch bookmarks from a Karakeep instance with pagination support *)
let fetch_bookmarks ~sw ~env ~api_key ?(limit=50) ?(offset=0) ?cursor ?(include_content=false) ?filter_tags base_url =
···
List.map (fun tag -> Uri.pct_encode ~component:`Query_key tag) tags
in
let tags_param = String.concat "," encoded_tags in
+
Log.debug (fun m -> m "Adding tags filter: %s" tags_param);
url ^ "&tags=" ^ tags_param
| _ -> url
in
···
let headers = Requests.Headers.empty
|> Requests.Headers.set "Authorization" ("Bearer " ^ api_key) in
+
Log.debug (fun m -> m "Fetching bookmarks from: %s" url);
try
let response = Requests.One.get ~sw ~clock:env#clock ~net:env#net ~headers url in
let status_code = Requests.Response.status_code response in
if status_code = 200 then begin
let body_str = Requests.Response.body response |> Eio.Flow.read_all in
+
Log.debug (fun m -> m "Received %d bytes of response data" (String.length body_str));
try
let json = J.from_string body_str in
parse_bookmark_response json
with e ->
+
Log.err (fun m -> m "JSON parsing error: %s" (Printexc.to_string e));
+
Log.debug (fun m -> m "Response body (first 200 chars): %s"
(if String.length body_str > 200 then String.sub body_str 0 200 ^ "..." else body_str));
raise e
end else begin
+
Log.err (fun m -> m "HTTP error %d" status_code);
failwith (Fmt.str "HTTP error: %d" status_code)
end
with e ->
+
Log.err (fun m -> m "Network error: %s" (Printexc.to_string e));
raise e
(** Fetch all bookmarks from a Karakeep instance using pagination *)
+
let fetch_all_bookmarks ~sw ~env ~api_key ?(page_size=50) ?max_pages ?max_bookmarks ?filter_tags ?(include_content=false) base_url =
+
let rec fetch_pages page_num cursor acc =
+
(* Check if we've reached the max_bookmarks limit *)
+
let reached_limit = match max_bookmarks with
+
| Some max when List.length acc >= max ->
+
Log.debug (fun m -> m "Reached max_bookmarks limit (%d)" max);
+
true
+
| _ -> false
in
+
if reached_limit then
+
acc
+
else begin
+
Log.debug (fun m -> m "Fetching page %d" page_num);
+
let response =
+
match cursor with
+
| Some cursor_str -> fetch_bookmarks ~sw ~env ~api_key ~limit:page_size ~cursor:cursor_str ~include_content ?filter_tags base_url
+
| None -> fetch_bookmarks ~sw ~env ~api_key ~limit:page_size ~offset:(page_num * page_size) ~include_content ?filter_tags base_url
+
in
+
let all_bookmarks = acc @ response.data in
+
Log.debug (fun m -> m "Fetched %d bookmarks this page, %d total so far"
+
(List.length response.data) (List.length all_bookmarks));
+
+
(* Truncate to max_bookmarks if needed *)
+
let all_bookmarks = match max_bookmarks with
+
| Some max when List.length all_bookmarks > max ->
+
Log.debug (fun m -> m "Truncating to max_bookmarks (%d)" max);
+
List.filteri (fun i _ -> i < max) all_bookmarks
+
| _ -> all_bookmarks
+
in
+
+
(* Determine if more pages are available:
+
- If next_cursor is present, there are definitely more pages
+
- If no next_cursor and we got fewer items than page_size, we're done
+
- If no next_cursor and total is reliable (> current count), there may be more *)
+
let more_available =
+
match response.next_cursor with
+
| Some _ ->
+
Log.debug (fun m -> m "More pages available (next_cursor present)");
+
true
+
| None ->
+
let current_count = List.length all_bookmarks in
+
let got_full_page = List.length response.data = page_size in
+
let total_indicates_more = response.total > current_count in
+
(* If we got a full page and total indicates more, continue *)
+
let has_more = got_full_page && total_indicates_more in
+
if has_more then
+
Log.debug (fun m -> m "More pages likely available (%d fetched < %d total)"
+
current_count response.total)
+
else
+
Log.debug (fun m -> m "No more pages (got %d items, total=%d)"
+
(List.length response.data) response.total);
+
has_more
+
in
+
let under_max_pages = match max_pages with
+
| None -> true
+
| Some max -> page_num + 1 < max
+
in
+
+
let under_max_bookmarks = match max_bookmarks with
+
| None -> true
+
| Some max -> List.length all_bookmarks < max
+
in
+
if more_available && under_max_pages && under_max_bookmarks then
+
fetch_pages (page_num + 1) response.next_cursor all_bookmarks
+
else begin
+
Log.debug (fun m -> m "Pagination complete: fetched %d total bookmarks" (List.length all_bookmarks));
+
all_bookmarks
+
end
+
end
in
+
fetch_pages 0 None []
(** Fetch detailed information for a single bookmark by ID *)
let fetch_bookmark_details ~sw ~env ~api_key base_url bookmark_id =
+2
stack/karakeepe/karakeepe.mli
···
@param api_key API key for authentication
@param page_size Number of bookmarks to fetch per page (default: 50)
@param max_pages Maximum number of pages to fetch (None for all pages)
@param filter_tags Optional list of tags to filter by
@param include_content Whether to include full content (default: false)
@param base_url Base URL of the Karakeep instance
···
api_key:string ->
?page_size:int ->
?max_pages:int ->
?filter_tags:string list ->
?include_content:bool ->
string ->
···
@param api_key API key for authentication
@param page_size Number of bookmarks to fetch per page (default: 50)
@param max_pages Maximum number of pages to fetch (None for all pages)
+
@param max_bookmarks Maximum total number of bookmarks to return (None for all)
@param filter_tags Optional list of tags to filter by
@param include_content Whether to include full content (default: false)
@param base_url Base URL of the Karakeep instance
···
api_key:string ->
?page_size:int ->
?max_pages:int ->
+
?max_bookmarks:int ->
?filter_tags:string list ->
?include_content:bool ->
string ->
+13 -81
stack/xdge/lib/xdge.mli
···
(* config is now <fs:$HOME/.config/myapp> or the overridden path *)
]}
-
{b Note:} All directories are created with permissions 0o755 if they don't exist,
except for runtime directories which are created with 0o700 permissions and
validated according to the XDG specification.
···
- [XDG_CONFIG_HOME]: XDG standard variable
- Default: [$HOME/.config/{app_name}]
-
{b Example Files:}
-
- Application settings (e.g., [config.toml], [settings.json])
-
- User preferences
-
- UI customizations
-
@see <https://specifications.freedesktop.org/basedir-spec/latest/#variables> XDG_CONFIG_HOME specification *)
val config_dir : t -> Eio.Fs.dir_ty Eio.Path.t
···
- Network response caches
- Temporary computation results
-
{b Note:} Users may clear cache directories to free disk space, so
always check for cache validity and be prepared to regenerate data.
@see <https://specifications.freedesktop.org/basedir-spec/latest/#variables> XDG_CACHE_HOME specification *)
···
- Lock files
- Small process communication files
-
{b Important:} This may return [None] if no suitable runtime directory
is available. Applications should handle this gracefully, perhaps by
falling back to [/tmp] with appropriate security measures.
···
precedence over later ones. When looking for a configuration file,
search {!config_dir} first, then each directory in this list.
-
{b Example Usage:}
-
- System-wide default configurations
-
- Distribution-provided settings
-
- Site-wide customizations
-
@see <https://specifications.freedesktop.org/basedir-spec/latest/#variables> XDG_CONFIG_DIRS specification *)
val config_dirs : t -> Eio.Fs.dir_ty Eio.Path.t list
···
1. User config directory ({!config_dir})
2. System config directories ({!config_dirs}) in preference order
-
{b Example:}
-
{[
-
match Xdge.find_config_file xdg "myapp.conf" with
-
| Some path -> Printf.printf "Found config at: %s\n" (Eio.Path.native_exn path)
-
| None -> Printf.printf "No config file found\n"
-
]} *)
val find_config_file : t -> string -> Eio.Fs.dir_ty Eio.Path.t option
(** [find_data_file t filename] searches for a data file following XDG precedence.
···
1. User data directory ({!data_dir})
2. System data directories ({!data_dirs}) in preference order
-
{b Example:}
-
{[
-
match Xdge.find_data_file xdg "templates/default.txt" with
-
| Some path -> (* read from path *)
-
| None -> (* use built-in default *)
-
]} *)
val find_data_file : t -> string -> Eio.Fs.dir_ty Eio.Path.t option
(** {1 Pretty Printing} *)
···
- Normal: Multi-line detailed view of all directories
- Brief: Single line showing app name and key directories
- With sources: Adds annotations showing where each path came from
-
-
{b Example:}
-
{[
-
(* Normal output *)
-
Format.printf "%a" Xdge.pp xdg
-
-
(* Brief output *)
-
Format.printf "%a" (Xdge.pp ~brief:true) xdg
-
-
(* Show sources *)
-
Format.printf "%a" (Xdge.pp ~sources:true) xdg
-
]} *)
val pp : ?brief:bool -> ?sources:bool -> Format.formatter -> t -> unit
(** {1 Cmdliner Integration} *)
···
type xdg_t = t
(** Cmdliner integration for XDG directory configuration.
-
This module provides seamless integration with the Cmdliner library,
allowing XDG directories to be configured via command-line arguments
-
while respecting the precedence of environment variables.
-
-
{b Features:}
-
- Automatic command-line flag generation for each directory type
-
- Environment variable detection and precedence handling
-
- Source tracking for debugging configuration issues
-
- Pretty printing of configuration for --help output *)
-
(** Complete XDG configuration gathered from command-line and environment.
This contains all XDG directory paths along with their sources,
as determined by command-line arguments and environment variables. *)
···
+ Application-specific variable (e.g., [MYAPP_CONFIG_DIR])
+ XDG standard variable (e.g., [XDG_CONFIG_HOME])
+ Default value
-
-
{b Example - All directories:}
-
{[
-
let open Cmdliner in
-
let xdg_term = Cmd.term "myapp" env#fs () in
-
let main_term = Term.(const main $ xdg_term $ other_args) in
-
(* ... *)
-
]}
-
-
{b Example - Only cache directory:}
-
{[
-
let open Cmdliner in
-
let xdg_term = Cmd.term "myapp" env#fs ~dirs:[`Cache] () in
-
let main_term = Term.(const main $ xdg_term $ other_args) in
-
(* ... *)
-
]} *)
val term : string -> Eio.Fs.dir_ty Eio.Path.t ->
?dirs:dir list ->
unit -> (xdg_t * t) Cmdliner.Term.t
···
+ Application-specific variable (e.g., [MYAPP_CACHE_DIR])
+ XDG standard variable ([XDG_CACHE_HOME])
+ Default value ([$HOME/.cache/{app_name}])
-
-
{b Example:}
-
{[
-
let cache_dir_term = Xdge.Cmd.cache_term "myapp" in
-
let main cache_dir other_args =
-
(* use cache_dir as a string path *)
-
in
-
let main_term = Term.(const main $ cache_dir_term $ other_args) in
-
]} *)
val cache_term : string -> string Cmdliner.Term.t
(** [env_docs app_name] generates documentation for environment variables.
···
- Application-specific environment variables
- XDG standard environment variables
- Default values for each directory type
-
-
{b Example:}
-
{[
-
let env_section = Cmdliner.Cmd.Env.info (env_docs "myapp")
-
]} *)
val env_docs : string -> string
(** [pp ppf config] pretty prints a Cmdliner configuration.
···
issues or displaying the current configuration to users.
@param ppf The formatter to print to
-
@param config The configuration to print
-
-
{b Output Format:}
-
Shows each directory with its path (if set) and source annotation
-
in a color-coded format for easy reading. *)
val pp : Format.formatter -> t -> unit
end
···
(* config is now <fs:$HOME/.config/myapp> or the overridden path *)
]}
+
All directories are created with permissions 0o755 if they don't exist,
except for runtime directories which are created with 0o700 permissions and
validated according to the XDG specification.
···
- [XDG_CONFIG_HOME]: XDG standard variable
- Default: [$HOME/.config/{app_name}]
@see <https://specifications.freedesktop.org/basedir-spec/latest/#variables> XDG_CONFIG_HOME specification *)
val config_dir : t -> Eio.Fs.dir_ty Eio.Path.t
···
- Network response caches
- Temporary computation results
+
Users may clear cache directories to free disk space, so
always check for cache validity and be prepared to regenerate data.
@see <https://specifications.freedesktop.org/basedir-spec/latest/#variables> XDG_CACHE_HOME specification *)
···
- Lock files
- Small process communication files
+
This may return [None] if no suitable runtime directory
is available. Applications should handle this gracefully, perhaps by
falling back to [/tmp] with appropriate security measures.
···
precedence over later ones. When looking for a configuration file,
search {!config_dir} first, then each directory in this list.
@see <https://specifications.freedesktop.org/basedir-spec/latest/#variables> XDG_CONFIG_DIRS specification *)
val config_dirs : t -> Eio.Fs.dir_ty Eio.Path.t list
···
1. User config directory ({!config_dir})
2. System config directories ({!config_dirs}) in preference order
+
*)
val find_config_file : t -> string -> Eio.Fs.dir_ty Eio.Path.t option
(** [find_data_file t filename] searches for a data file following XDG precedence.
···
1. User data directory ({!data_dir})
2. System data directories ({!data_dirs}) in preference order
+
*)
val find_data_file : t -> string -> Eio.Fs.dir_ty Eio.Path.t option
(** {1 Pretty Printing} *)
···
- Normal: Multi-line detailed view of all directories
- Brief: Single line showing app name and key directories
- With sources: Adds annotations showing where each path came from
+
*)
val pp : ?brief:bool -> ?sources:bool -> Format.formatter -> t -> unit
(** {1 Cmdliner Integration} *)
···
type xdg_t = t
(** Cmdliner integration for XDG directory configuration.
+
This module provides integration with the Cmdliner library,
allowing XDG directories to be configured via command-line arguments
+
while respecting the precedence of environment variables. *)
+
(** Type of XDG configuration gathered from command-line and environment.
This contains all XDG directory paths along with their sources,
as determined by command-line arguments and environment variables. *)
···
+ Application-specific variable (e.g., [MYAPP_CONFIG_DIR])
+ XDG standard variable (e.g., [XDG_CONFIG_HOME])
+ Default value
+
*)
val term : string -> Eio.Fs.dir_ty Eio.Path.t ->
?dirs:dir list ->
unit -> (xdg_t * t) Cmdliner.Term.t
···
+ Application-specific variable (e.g., [MYAPP_CACHE_DIR])
+ XDG standard variable ([XDG_CACHE_HOME])
+ Default value ([$HOME/.cache/{app_name}])
+
*)
val cache_term : string -> string Cmdliner.Term.t
(** [env_docs app_name] generates documentation for environment variables.
···
- Application-specific environment variables
- XDG standard environment variables
- Default values for each directory type
+
*)
val env_docs : string -> string
(** [pp ppf config] pretty prints a Cmdliner configuration.
···
issues or displaying the current configuration to users.
@param ppf The formatter to print to
+
@param config The configuration to print *)
val pp : Format.formatter -> t -> unit
end