From 653a004195f4a0447daa3246d6f64de3930a120e Mon Sep 17 00:00:00 2001 From: rcmerci Date: Tue, 7 Apr 2026 23:49:58 +0800 Subject: [PATCH] enhance(cli): display width in all list-* cmds --- ...079-logseq-cli-list-title-display-width.md | 246 ++++++++++++++++++ docs/cli/logseq-cli.md | 3 +- package.json | 1 + src/main/logseq/cli/config.cljs | 25 +- src/main/logseq/cli/format.cljs | 112 ++++++-- src/test/logseq/cli/config_test.cljs | 24 +- src/test/logseq/cli/format_test.cljs | 66 ++++- 7 files changed, 445 insertions(+), 32 deletions(-) create mode 100644 docs/agent-guide/079-logseq-cli-list-title-display-width.md diff --git a/docs/agent-guide/079-logseq-cli-list-title-display-width.md b/docs/agent-guide/079-logseq-cli-list-title-display-width.md new file mode 100644 index 0000000000..e0c6876191 --- /dev/null +++ b/docs/agent-guide/079-logseq-cli-list-title-display-width.md @@ -0,0 +1,246 @@ +# Logseq CLI List Title Display Width Implementation Plan + +Goal: Make every `TITLE` column in `logseq list *` human output use a fixed maximum visual width with correct CJK-aware width handling via `string-width`. + +Architecture: Keep list data retrieval unchanged in db-worker-node thread APIs and implement presentation-only changes in the CLI formatter layer. + +Architecture: Introduce a display-width-aware truncation and padding path that is applied to list title cells before table rendering. + +Tech Stack: ClojureScript, `logseq.cli.format`, `logseq.cli.command.list`, db-worker thread APIs in `frontend.worker.db-core`, JavaScript `string-width`. + +Related: Builds on `docs/agent-guide/062-logseq-cli-list-default-sort-updated-at.md`, `docs/agent-guide/066-logseq-cli-list-property-cardinality-column.md`, `docs/agent-guide/078-logseq-cli-task-subcommands.md`, and `docs/cli/logseq-cli.md`. + +## Problem statement + +Current human table rendering in `src/main/logseq/cli/format.cljs` uses `(count text)` for width calculation and right padding. + +`count` is code-point length, not terminal display width. + +This breaks alignment when titles contain CJK or mixed-width text. + +Current `list` human output also allows unbounded title length, which can push important columns out of view for `list page`, `list tag`, `list property`, and `list task`. + +We need a stable, readable table contract where `TITLE` is capped to a reasonable visual width and aligned correctly for mixed-language content. + +## Current baseline from implementation + +The active CLI stack is the `src/main/logseq/cli/*` implementation, not the legacy `deps/cli` command stack. + +List command data is produced by db-worker helpers and returned through thread APIs without formatter concerns. + +The relevant path is: + +```text +logseq list + -> /Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/list.cljs + -> transport/invoke :thread-api/cli-list-* + -> /Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/db_core.cljs + -> /Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/common/db_worker.cljs + -> /Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/format.cljs +``` + +`TITLE` columns are defined in formatter column configs in `src/main/logseq/cli/format.cljs`. + +All table rendering goes through `render-table` and `format-counted-table` in the same file. + +## Scope + +In scope commands are: + +| Command | Human output table has `TITLE` column | Source formatter function | +| --- | --- | --- | +| `logseq list page` | Yes | `format-list-page` | +| `logseq list tag` | Yes | `format-list-tag` | +| `logseq list property` | Yes | `format-list-property` | +| `logseq list task` | Yes | `format-list-task` | + +The default max visual width target for title is `40` columns. + +The value is configurable in `cli.edn` via a new CLI config key. + +The cap is measured by display width, not character count. + +Out of scope commands include `search *`, `query list`, `graph list`, and `server list`. + +Out of scope outputs include JSON and EDN payload shape changes. + +## Design decisions + +### 1) Keep db-worker-node payloads unchanged + +No changes are required in `src/main/logseq/cli/common/db_worker.cljs` or thread API wiring in `src/main/frontend/worker/db_core.cljs`. + +db-worker should continue returning full `:block/title` values. + +Human formatting is the only layer that applies visual truncation. + +### 2) Add explicit title max-width policy in formatter + +Introduce a formatter default constant such as `list-human-title-max-display-width-default` with value `40`. + +Read an optional override from resolved CLI config using a new `cli.edn` key `:list-title-max-display-width`. + +Apply this policy only to `TITLE` extractors in list column definitions. + +Truncated values append a suffix `…` and remain within the max display width. + +### 3) Use `string-width` for display width operations + +Add npm interop in `src/main/logseq/cli/format.cljs` for `string-width`. + +Use `string-width` for: + +- Width measurement during truncation. +- Width measurement during table column width calculation. +- Padding logic in `pad-right` or equivalent. + +This ensures CJK and mixed-width titles align with other columns. + +### 4) Preserve existing list data semantics + +Sorting, filtering, `--fields`, `--limit`, and `--offset` behavior in `src/main/logseq/cli/command/list.cljs` remain unchanged. + +Only rendered human strings are affected. + +### 5) Keep non-list command behavior stable + +Prefer implementing title truncation in list-specific extractors. + +If `render-table` is upgraded to display-width-aware padding globally, verify non-list tables are unaffected except improved alignment. + +## Proposed implementation steps + +1. Invoke `@test-driven-development` and add failing formatter tests for long ASCII title truncation in each list command. + +2. Add failing formatter tests for long CJK and mixed CJK/ASCII titles in each list command. + +3. Add failing config resolution tests in `src/test/logseq/cli/config_test.cljs` for `:list-title-max-display-width`, including default fallback to `40` and valid `cli.edn` override. + +4. Add a focused formatter unit test for truncation helper behavior ensuring output display width is `<= 40` and suffix handling is correct. + +5. Add a focused formatter unit test for width-aware padding behavior with CJK text so column alignment is deterministic. + +6. Add config normalization helpers in `src/main/logseq/cli/config.cljs` to parse and validate `:list-title-max-display-width` as a positive integer. + +7. Add `string-width` import and helper functions in `src/main/logseq/cli/format.cljs`. + +8. Add title truncation helper and wire it into the `TITLE` extractor entries in `list-page-columns`, `list-tag-columns`, `list-property-columns`, and `list-task-columns`. + +9. Replace `(count ...)`-based width and padding internals in table rendering with display-width-aware logic. + +10. Run formatter and config tests and update exact spacing snapshots where needed. + +11. Update user-facing docs in `docs/cli/logseq-cli.md` to mention that list human `TITLE` uses default width `40`, supports `cli.edn` override, and always truncates with `…`. + +12. Optionally add one CLI e2e non-sync human-output case for long mixed-width title rendering if we want package-level regression protection. + +## Files to modify + +- `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/config.cljs`. +- `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/config_test.cljs`. +- `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/format.cljs`. +- `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/format_test.cljs`. +- `/Users/rcmerci/gh-repos/logseq/docs/cli/logseq-cli.md`. +- `/Users/rcmerci/gh-repos/logseq/cli-e2e/spec/non_sync_cases.edn` (optional, only if we decide to cover human output end-to-end). +- `/Users/rcmerci/gh-repos/logseq/package.json` (only if `string-width` is added as an explicit direct dependency). + +## Edge cases to cover + +A title with only CJK characters that exceeds max width. + +A title with alternating CJK and ASCII characters. + +A title exactly at width boundary. + +A title one display cell over boundary. + +A title containing emoji or variation selectors. + +A nil title where current logic falls back to `-`. + +A title containing newlines where current multiline row rendering is preserved. + +A very short title where no extra truncation marker appears. + +An invalid `cli.edn` width value such as `0`, negative numbers, or non-numeric values that must fall back to default `40`. + +## Risks and mitigation + +Risk: Moving width calculations to display width can shift spacing in non-list tables. + +Mitigation: Add or adjust tests for affected command outputs and constrain behavioral changes to alignment only. + +Risk: `string-width` import style may vary by bundling mode. + +Mitigation: Verify in unit tests and CLI runtime, and choose the interop form that matches current CJS usage conventions in the repo. + +Risk: New truncation can reduce discoverability of long titles. + +Mitigation: Keep JSON and EDN outputs untruncated and document this behavior clearly. + +Risk: Invalid `cli.edn` width values can produce inconsistent rendering if not normalized. + +Mitigation: Parse as positive integer with fallback to default `40` and add config tests for invalid values. + +## Open questions + +No open question. + +Decisions locked for this plan are default width `40`, `cli.edn` configurability, truncation suffix `…`, and list-only scope. + +## Testing Plan + +I will add formatter behavior tests in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/format_test.cljs` that verify title truncation and alignment for `list page`, `list tag`, `list property`, and `list task` human output. + +I will add helper-level tests in the same test namespace to validate width-aware truncation and padding with CJK and mixed-width samples. + +I will add config tests in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/config_test.cljs` to validate default value `40`, `cli.edn` override, and invalid-value fallback behavior. + +I will run focused tests first with `bb dev:test -v logseq.cli.format-test/test-human-output-list-page`, `bb dev:test -v logseq.cli.format-test/test-human-output-list-tag-property`, and `bb dev:test -v logseq.cli.format-test/test-human-output-list-task`. + +I will run broader CLI tests with `bb dev:test -v logseq.cli.format-test` and config tests with `bb dev:test -v logseq.cli.config-test`. + +If e2e coverage is added, I will run `bb -f cli-e2e/bb.edn test --skip-build`. + +I will finish with `bb dev:lint-and-test`. + +NOTE: I will write *all* tests before I add any implementation behavior. + +## Step-by-step execution checklist + +| Step | Action | Verification | +| --- | --- | --- | +| 1 | Add failing list title truncation tests. | Tests fail with current unbounded title output. | +| 2 | Add failing CJK width alignment tests. | Tests fail due current `count`-based width logic. | +| 3 | Add failing config tests for `:list-title-max-display-width`. | Tests fail before config parsing is implemented. | +| 4 | Implement config parsing with default `40` and invalid fallback. | Config tests pass for default and override behavior. | +| 5 | Implement `string-width` helpers. | Helper tests pass. | +| 6 | Implement title truncation in list column extractors. | List tests show bounded title width and `…` suffix. | +| 7 | Switch table padding/width to display-width-aware logic. | CJK alignment tests pass. | +| 8 | Update docs and optional e2e. | Docs mention truncation behavior and tests stay green. | +| 9 | Run full regression. | `bb dev:lint-and-test` passes. | + +## Testing Details + +The tests validate observable CLI behavior by asserting final human output strings for list commands, including truncation marker presence and table alignment under mixed-width input. + +The tests do not assert internal helper data structures beyond what is needed to validate final output semantics. + +## Implementation Details + +- Keep default max title width as formatter constant `40`. +- Allow override from `cli.edn` key `:list-title-max-display-width`. +- Parse config width as positive integer and fall back to default on invalid values. +- Truncate by display width instead of character count. +- Keep truncation policy limited to list `TITLE` columns. +- Always use `…` as truncation suffix in human list output. +- Preserve db-worker payload contracts and thread API names. +- Preserve JSON and EDN full title output. +- Ensure multiline table behavior is not regressed. +- Document human-output-only truncation and config override in CLI docs. + +## Question + +No blocking question for implementation. + +--- diff --git a/docs/cli/logseq-cli.md b/docs/cli/logseq-cli.md index 9b8d242ead..dd48b36358 100644 --- a/docs/cli/logseq-cli.md +++ b/docs/cli/logseq-cli.md @@ -57,6 +57,7 @@ Supported keys include: - `:data-dir` - `:timeout-ms` - `:output-format` (use `:json` or `:edn` for scripting) +- `:list-title-max-display-width` (human `list *` TITLE max display width, default `40`) - sync config persisted via `sync config set|get|unset`: `:ws-url`, `:http-base` Legacy migration note: @@ -288,7 +289,7 @@ Output formats: 139ms └── cli.execute-action 129ms └── transport.invoke:thread-api/cli-list-pages ``` -- Human output is plain text. List/search commands render tables with a final `Count: N` line. For list and search subcommands, the ID column uses `:db/id` (not UUID). If `:db/ident` exists, an `IDENT` column is included. `list property` includes dedicated `TYPE` and `CARDINALITY` columns. Search table columns are `ID` and `TITLE`. Block titles can include multiple lines; multi-line rows align additional lines under the `TITLE` column. Times such as list `UPDATED-AT`/`CREATED-AT` and `graph info` `Created at` are shown in human-friendly relative form. Errors include error codes and may include a `Hint:` line. Use `--output json|edn` for structured output. +- Human output is plain text. List/search commands render tables with a final `Count: N` line. For list and search subcommands, the ID column uses `:db/id` (not UUID). If `:db/ident` exists, an `IDENT` column is included. `list property` includes dedicated `TYPE` and `CARDINALITY` columns. Search table columns are `ID` and `TITLE`. For `list page|tag|property|task` in human output, the `TITLE` column is display-width-aware (CJK-safe), defaults to max width `40`, and truncates overflow with `…`; set `:list-title-max-display-width` in `cli.edn` to override. JSON/EDN outputs keep full titles (no truncation). Block titles can include multiple lines; multi-line rows align additional lines under the `TITLE` column. Times such as list `UPDATED-AT`/`CREATED-AT` and `graph info` `Created at` are shown in human-friendly relative form. Errors include error codes and may include a `Hint:` line. Use `--output json|edn` for structured output. - `example` human output includes `Selector`, `Matched commands`, and `Examples` sections. Structured output (`json`/`edn`) includes `selector`, `matched-commands`, `examples`, and `message` fields under `data`. - `sync download` progress lines are streamed to stdout only when progress is enabled. In `json`/`edn` mode, progress is disabled by default unless `--progress true` is provided. - JSON machine output preserves namespaced keyword semantics: diff --git a/package.json b/package.json index ddf35954f9..03bc14185e 100644 --- a/package.json +++ b/package.json @@ -186,6 +186,7 @@ "remove-accents": "0.5.0", "sanitize-filename": "1.6.4", "send-intent": "^7.0.0", + "string-width": "8.2.0", "url": "^0.11.4", "util": "^0.12.5" }, diff --git a/src/main/logseq/cli/config.cljs b/src/main/logseq/cli/config.cljs index 6f7c6370de..875b7dc9e5 100644 --- a/src/main/logseq/cli/config.cljs +++ b/src/main/logseq/cli/config.cljs @@ -13,6 +13,23 @@ (when (and (some? value) (not (string/blank? value))) (js/parseInt value 10))) +(def ^:private list-title-max-display-width-default 40) + +(defn- parse-positive-int + [value] + (cond + (and (number? value) + (integer? value) + (pos? value)) + value + + (string? value) + (let [trimmed (string/trim value)] + (when (re-matches #"[1-9]\d*" trimmed) + (js/parseInt trimmed 10))) + + :else nil)) + (def ^:private output-formats #{:human :json :edn}) @@ -98,6 +115,7 @@ (let [defaults {:timeout-ms 10000 :login-timeout-ms 300000 :logout-timeout-ms 120000 + :list-title-max-display-width list-title-max-display-width-default :output-format nil :data-dir (common-graph/get-default-graphs-dir) :ws-url "wss://api-staging.logseq.io/sync/%s" @@ -114,6 +132,9 @@ (parse-output-format (:output env)) (parse-output-format (:output-format file-config)) (parse-output-format (:output file-config))) - merged (merge defaults file-config env opts {:config-path config-path})] + merged (merge defaults file-config env opts {:config-path config-path}) + list-title-max-display-width (or (parse-positive-int (:list-title-max-display-width merged)) + list-title-max-display-width-default)] (cond-> merged - output-format (assoc :output-format output-format)))) + output-format (assoc :output-format output-format) + true (assoc :list-title-max-display-width list-title-max-display-width)))) diff --git a/src/main/logseq/cli/format.cljs b/src/main/logseq/cli/format.cljs index 5d44e3e7d2..cf8a3004e5 100644 --- a/src/main/logseq/cli/format.cljs +++ b/src/main/logseq/cli/format.cljs @@ -5,7 +5,8 @@ [clojure.walk :as walk] [logseq.cli.command.core :as command-core] [logseq.cli.style :as style] - [logseq.common.util :as common-util])) + [logseq.common.util :as common-util] + ["string-width" :default string-width])) (defn- keyword->json-string [kw] @@ -63,13 +64,9 @@ (set! (.-data obj) (clj->js (normalize-json data)))))) (js/JSON.stringify obj))) -(defn- pad-right - [value width] - (let [text (str value) - missing (- width (count text))] - (if (pos? missing) - (str text (apply str (repeat missing " "))) - text))) +(def ^:private list-human-title-max-display-width-default 40) +(def ^:private truncation-suffix "…") +(def ^:private truncation-suffix-width (string-width truncation-suffix)) (defn- normalize-cell [value] @@ -78,6 +75,53 @@ (keyword? value) (str value) :else (str value))) +(defn- display-width + [value] + (string-width (normalize-cell value))) + +(defn- pad-right + [value width] + (let [text (normalize-cell value) + missing (- width (display-width text))] + (if (pos? missing) + (str text (apply str (repeat missing " "))) + text))) + +(defn- take-to-display-width + [text max-width] + (loop [remaining-chars (seq (js/Array.from text)) + acc ""] + (if-let [ch (first remaining-chars)] + (let [candidate (str acc ch)] + (if (<= (display-width candidate) max-width) + (recur (next remaining-chars) candidate) + acc)) + acc))) + +(defn- truncate-line-to-display-width + [line max-width] + (if (<= (display-width line) max-width) + line + (if (<= max-width truncation-suffix-width) + (take-to-display-width truncation-suffix max-width) + (str (take-to-display-width line (- max-width truncation-suffix-width)) + truncation-suffix)))) + +(defn- truncate-title-to-display-width + [value max-width] + (let [max-width (max 1 max-width)] + (->> (string/split (normalize-cell value) #"\n" -1) + (map #(truncate-line-to-display-width % max-width)) + (string/join "\n")))) + +(defn- resolve-list-title-max-display-width + [value] + (if (and (number? value) + (integer? value) + (pos? value)) + value + list-human-title-max-display-width-default)) + (defn- render-table [headers rows] (let [split-lines (fn [value] @@ -89,8 +133,8 @@ (string/replace value #"\s+$" "")) widths (mapv (fn [idx header] (reduce max - (count header) - (mapcat #(map count (nth % idx)) normalized-rows))) + (display-width header) + (mapcat #(map display-width (nth % idx)) normalized-rows))) (range (count headers)) headers) render-row (fn [row] @@ -206,20 +250,28 @@ ["CREATED-AT" (fn [item now-ms] (human-ago (or (:created-at item) (:block/created-at item)) now-ms)) [:created-at :block/created-at]]]) (defn- format-list-dynamic - [items now-ms columns] + [items now-ms columns {:keys [title-max-display-width]}] (let [items (or items []) active (filterv (fn [[_ _ ks always?]] (or always? (apply items-have-key? items ks))) columns) headers (mapv first active) rows (mapv (fn [item] - (mapv (fn [[_ extractor _]] (extractor item now-ms)) active)) + (mapv (fn [[header extractor _]] + (let [cell (extractor item now-ms)] + (if (and (= header "TITLE") + (number? title-max-display-width)) + (truncate-title-to-display-width cell title-max-display-width) + cell))) + active)) items)] (format-counted-table headers rows))) (defn- format-list-page - [items now-ms] - (format-list-dynamic items now-ms list-page-columns)) + ([items now-ms] + (format-list-page items now-ms nil)) + ([items now-ms title-max-display-width] + (format-list-dynamic items now-ms list-page-columns {:title-max-display-width title-max-display-width}))) (defn- format-extends [classes] @@ -241,8 +293,10 @@ ["CREATED-AT" (fn [item now-ms] (human-ago (or (:created-at item) (:block/created-at item)) now-ms)) [:created-at :block/created-at]]]) (defn- format-list-tag - [items now-ms] - (format-list-dynamic items now-ms list-tag-columns)) + ([items now-ms] + (format-list-tag items now-ms nil)) + ([items now-ms title-max-display-width] + (format-list-dynamic items now-ms list-tag-columns {:title-max-display-width title-max-display-width}))) (defn- normalize-property-type [value] @@ -268,8 +322,10 @@ ["CREATED-AT" (fn [item now-ms] (human-ago (or (:created-at item) (:block/created-at item)) now-ms)) [:created-at :block/created-at]]]) (defn- format-list-property - [items now-ms] - (format-list-dynamic items now-ms list-property-columns)) + ([items now-ms] + (format-list-property items now-ms nil)) + ([items now-ms title-max-display-width] + (format-list-dynamic items now-ms list-property-columns {:title-max-display-width title-max-display-width}))) (defn- format-task-choice [value prefix] @@ -300,8 +356,10 @@ ["CREATED-AT" (fn [item now-ms] (human-ago (or (:created-at item) (:block/created-at item)) now-ms)) [:created-at :block/created-at] true]]) (defn- format-list-task - [items now-ms] - (format-list-dynamic items now-ms list-task-columns)) + ([items now-ms] + (format-list-task items now-ms nil)) + ([items now-ms title-max-display-width] + (format-list-dynamic items now-ms list-task-columns {:title-max-display-width title-max-display-width}))) (defn- quote-posix-shell [value] @@ -781,8 +839,10 @@ (string/join "\n" (into [header] check-lines)))) (defn- ->human - [{:keys [status data error command context human]} {:keys [now-ms graph data-dir]}] - (let [now-ms (or now-ms (js/Date.now))] + [{:keys [status data error command context human]} + {:keys [now-ms graph data-dir list-title-max-display-width]}] + (let [now-ms (or now-ms (js/Date.now)) + list-title-max-display-width (resolve-list-title-max-display-width list-title-max-display-width)] (case status :ok (case command @@ -809,10 +869,10 @@ :sync-config-unset (format-sync-config-unset data) :login (format-login data) :logout (format-logout data) - :list-page (format-list-page (:items data) now-ms) - :list-tag (format-list-tag (:items data) now-ms) - :list-property (format-list-property (:items data) now-ms) - :list-task (format-list-task (:items data) now-ms) + :list-page (format-list-page (:items data) now-ms list-title-max-display-width) + :list-tag (format-list-tag (:items data) now-ms list-title-max-display-width) + :list-property (format-list-property (:items data) now-ms list-title-max-display-width) + :list-task (format-list-task (:items data) now-ms list-title-max-display-width) (:search-block :search-page :search-property :search-tag) (format-list-page (:items data) now-ms) :upsert-block (format-upsert-block context (:result data)) diff --git a/src/test/logseq/cli/config_test.cljs b/src/test/logseq/cli/config_test.cljs index ff1c25abc4..f8f98981d1 100644 --- a/src/test/logseq/cli/config_test.cljs +++ b/src/test/logseq/cli/config_test.cljs @@ -1,6 +1,6 @@ (ns logseq.cli.config-test (:require [cljs.reader :as reader] - [cljs.test :refer [deftest is]] + [cljs.test :refer [deftest is testing]] [frontend.test.node-helper :as node-helper] [goog.object :as gobj] [logseq.cli.config :as config] @@ -98,7 +98,27 @@ (is (= "https://api-staging.logseq.io" (:http-base result))) (is (= 10000 (:timeout-ms result))) (is (= 300000 (:login-timeout-ms result))) - (is (= 120000 (:logout-timeout-ms result))))) + (is (= 120000 (:logout-timeout-ms result))) + (is (= 40 (:list-title-max-display-width result))))) + +(deftest test-list-title-max-display-width-config + (testing "reads valid list-title-max-display-width from cli.edn" + (let [dir (node-helper/create-tmp-dir) + cfg-path (node-path/join dir "cli.edn") + _ (fs/writeFileSync cfg-path "{:list-title-max-display-width 72}") + result (config/resolve-config {:config-path cfg-path})] + (is (= 72 (:list-title-max-display-width result))))) + + (testing "falls back to default when cli.edn value is invalid" + (doseq [[label file-value] + [["zero" "0"] + ["negative" "-3"] + ["non-numeric" "\"abc\""]]] + (let [dir (node-helper/create-tmp-dir) + cfg-path (node-path/join dir "cli.edn") + _ (fs/writeFileSync cfg-path (str "{:list-title-max-display-width " file-value "}")) + result (config/resolve-config {:config-path cfg-path})] + (is (= 40 (:list-title-max-display-width result)) label))))) (deftest test-update-config (let [dir (node-helper/create-tmp-dir "cli") diff --git a/src/test/logseq/cli/format_test.cljs b/src/test/logseq/cli/format_test.cljs index 2b775455f8..2243f508dd 100644 --- a/src/test/logseq/cli/format_test.cljs +++ b/src/test/logseq/cli/format_test.cljs @@ -4,7 +4,8 @@ [clojure.string :as string] [logseq.cli.command.show :as show-command] [logseq.cli.format :as format] - [logseq.cli.style :as style])) + [logseq.cli.style :as style] + ["string-width" :default string-width])) (deftest test-format-success (testing "json output via output-format" @@ -266,6 +267,69 @@ (is (string/includes? result "Alpha task")) (is (string/includes? result "Count: 1")))) +(deftest test-human-output-list-title-max-display-width + (doseq [[label command item] + [["list page truncates title by configured display width" + :list-page + {:db/id 1 + :block/title "ABCDEFGH" + :block/updated-at 90000 + :block/created-at 40000}] + ["list tag truncates title by configured display width" + :list-tag + {:db/id 2 + :block/title "ABCDEFGH" + :block/updated-at 90000 + :block/created-at 40000}] + ["list property truncates title by configured display width" + :list-property + {:db/id 3 + :block/title "ABCDEFGH" + :logseq.property/type :node + :db/cardinality :db.cardinality/one + :block/updated-at 90000 + :block/created-at 40000}] + ["list task truncates title by configured display width" + :list-task + {:db/id 4 + :block/title "ABCDEFGH" + :block/updated-at 90000 + :block/created-at 40000}]]] + (testing label + (let [result (format/format-result {:status :ok + :command command + :data {:items [item]}} + {:output-format nil + :now-ms 100000 + :list-title-max-display-width 6})] + (is (string/includes? result "ABCDE…")) + (is (not (string/includes? result "ABCDEFGH"))))))) + +(deftest test-human-output-list-cjk-title-alignment + (let [result (format/format-result {:status :ok + :command :list-page + :data {:items [{:db/id 1 + :block/title "ABCD" + :block/updated-at 90000 + :block/created-at 40000} + {:db/id 2 + :block/title "你好" + :block/updated-at 90000 + :block/created-at 40000}]}} + {:output-format nil + :now-ms 100000 + :list-title-max-display-width 10}) + lines (string/split-lines result) + line-1 (nth lines 1) + line-2 (nth lines 2) + updated-at-idx-1 (.indexOf line-1 "10s ago") + updated-at-idx-2 (.indexOf line-2 "10s ago") + prefix-width-1 (string-width (subs line-1 0 updated-at-idx-1)) + prefix-width-2 (string-width (subs line-2 0 updated-at-idx-2))] + (is (pos? updated-at-idx-1)) + (is (pos? updated-at-idx-2)) + (is (= prefix-width-1 prefix-width-2)))) + (deftest test-human-output-search (testing "search block renders the list table contract" (let [result (format/format-result {:status :ok