···3232 ]
3333 in
34343535- let list_cmd_term = Term.(const (fun () -> Sortal.Cmd.list_cmd ()) $ const ()) in
3535+ let graphics_term = Kgp_cmdliner.graphics_term in
3636+3737+ let list_cmd_term = Term.(const Sortal.Cmd.list_cmd $ graphics_term) in
3638 let list_cmd = run ~info:Sortal.Cmd.list_info list_cmd_term in
37393838- let show_cmd_term = Term.(const (fun handle -> Sortal.Cmd.show_cmd handle) $ Sortal.Cmd.handle_arg) in
4040+ let show_cmd_term = Term.(const Sortal.Cmd.show_cmd $ graphics_term $ Sortal.Cmd.handle_arg) in
3941 let show_cmd = run ~info:Sortal.Cmd.show_info show_cmd_term in
40424143 let search_cmd_term = Term.(const (fun query -> Sortal.Cmd.search_cmd query) $ Sortal.Cmd.query_arg) in
+33-36
lib/sortal_cmd.ml
···11open Cmdliner
2233-let image_columns = 9 (* columns for 4-row square-ish thumbnail + 3 padding *)
33+let image_columns = 11 (* columns for 4-row thumbnail (8 cols) + 3 padding *)
4455-let supports_kitty_graphics () =
66- let check_env var =
77- match Sys.getenv_opt var with
88- | Some _ -> true
99- | None -> false
1010- in
1111- let check_env_contains var substr =
1212- match Sys.getenv_opt var with
1313- | Some v -> String.lowercase_ascii v |> fun s ->
1414- String.length s >= String.length substr &&
1515- let rec check i =
1616- if i > String.length s - String.length substr then false
1717- else if String.sub s i (String.length substr) = substr then true
1818- else check (i + 1)
1919- in check 0
2020- | None -> false
2121- in
2222- check_env "KITTY_WINDOW_ID" ||
2323- check_env "WEZTERM_PANE" ||
2424- check_env "GHOSTTY_RESOURCES_DIR" ||
2525- check_env_contains "TERM_PROGRAM" "kitty" ||
2626- check_env_contains "TERM_PROGRAM" "wezterm" ||
2727- check_env_contains "TERM_PROGRAM" "ghostty" ||
2828- check_env_contains "TERM" "kitty"
55+(* Resolve graphics mode to determine if we should use graphics output *)
66+let use_graphics mode =
77+ Kgp.Terminal.supports_graphics mode
88+99+let next_image_id = ref 1
29103011let display_png_thumbnail path =
3112 let png_data = Eio.Path.load path in
3232- let placement = Kgp.Placement.make ~rows:4 ~cursor:`Static () in
3333- let cmd = Kgp.transmit_and_display ~format:`Png ~placement () in
3434- Kgp.to_string cmd ~data:png_data
1313+ let rows = 4 in
1414+ let cols = 8 in
1515+ (* Direct rendering - cursor stays in place after image *)
1616+ let placement = Kgp.Placement.make ~rows ~columns:cols ~cursor:`Static () in
1717+ let cmd = Kgp.transmit_and_display ~format:`Png ~placement ~quiet:`Silent () in
1818+ let buf = Buffer.create 4096 in
1919+ Kgp.write_tmux buf cmd ~data:png_data;
2020+ Buffer.contents buf
35213622let display_block_placeholder () =
3723 (* 4 rows of patterned block characters as fallback *)
···48344935(* 1-row thumbnail for listings *)
5036let display_small_thumbnail path =
3737+ let image_id = !next_image_id in
3838+ incr next_image_id;
5139 let png_data = Eio.Path.load path in
5252- let placement = Kgp.Placement.make ~rows:1 () in
5353- let cmd = Kgp.transmit_and_display ~format:`Png ~placement () in
5454- Kgp.to_string cmd ~data:png_data
4040+ let rows = 1 in
4141+ let cols = 2 in
4242+ (* Transmit image with unicode placeholder virtual placement, suppress responses *)
4343+ let placement = Kgp.Placement.make ~rows ~columns:cols ~unicode_placeholder:true () in
4444+ let cmd = Kgp.transmit_and_display ~image_id ~format:`Png ~placement ~quiet:`Silent () in
4545+ let buf = Buffer.create 4096 in
4646+ (* Use tmux-aware write that wraps for passthrough if needed *)
4747+ Kgp.write_tmux buf cmd ~data:png_data;
4848+ (* Write unicode placeholders (these are just text, no wrapping needed) *)
4949+ Kgp.Unicode_placeholder.write buf ~image_id ~rows ~cols ();
5050+ Buffer.add_char buf ' '; (* spacing after thumbnail *)
5151+ Buffer.contents buf
55525653let small_placeholder () = "▓░ " (* 1-row patterned placeholder *)
57545858-let list_cmd () xdg =
5555+let list_cmd mode xdg =
5956 let store = Sortal_store.create_from_xdg xdg in
6057 let contacts = Sortal_store.list store in
6158 let sorted = List.sort Sortal_contact.compare contacts in
6262- let use_graphics = supports_kitty_graphics () in
5959+ let graphics = use_graphics mode in
6360 Printf.printf "Total contacts: %d\n" (List.length sorted);
6461 List.iter (fun c ->
6562 (match Sortal_store.png_thumbnail_path store c with
6663 | Some path ->
6767- if use_graphics then
6464+ if graphics then
6865 print_string (display_small_thumbnail path)
6966 else
7067 print_string (small_placeholder ())
···7471 ) sorted;
7572 0
76737777-let show_cmd handle xdg =
7474+let show_cmd mode handle xdg =
7875 let store = Sortal_store.create_from_xdg xdg in
7976 match Sortal_store.lookup store handle with
8077 | Some c ->
8178 let has_thumbnail = match Sortal_store.png_thumbnail_path store c with
8279 | Some path ->
8383- if supports_kitty_graphics () then
8080+ if use_graphics mode then
8481 print_string (display_png_thumbnail path)
8582 else
8683 print_string (display_block_placeholder ());
+6-4
lib/sortal_cmd.mli
···5566(** {1 Command Implementations} *)
7788-(** [list_cmd] is a Cmdliner command that lists all contacts.
88+(** [list_cmd mode] is a Cmdliner command that lists all contacts.
991010+ @param mode The graphics mode to use for thumbnails.
1011 Usage: Integrate into your CLI with [Cmd.group] or use standalone.
1112 Returns a function that takes an XDG context and returns an exit code. *)
1212-val list_cmd : unit -> (Xdge.t -> int)
1313+val list_cmd : Kgp.Terminal.graphics_mode -> (Xdge.t -> int)
13141414-(** [show_cmd handle] creates a command to show detailed contact information.
1515+(** [show_cmd mode handle] creates a command to show detailed contact information.
15161717+ @param mode The graphics mode to use for thumbnails.
1618 @param handle The contact handle to display *)
1717-val show_cmd : string -> (Xdge.t -> int)
1919+val show_cmd : Kgp.Terminal.graphics_mode -> string -> (Xdge.t -> int)
18201921(** [search_cmd query] creates a command to search contacts by name.
2022