Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ logs/
/mgrep
/mgit
/demo
/yu

# Local notes (not tracked)
local/
100 changes: 66 additions & 34 deletions docs/argmojo_overall_planning.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Comment out unreleased changes here. This file will be edited just before each r
5. Add `.remainder()` builder method on `Argument`. A remainder positional consumes **all** remaining tokens (including ones starting with `-`), similar to argparse `nargs=REMAINDER` or clap `trailing_var_arg`. At most one remainder positional is allowed per command and it must be the last positional (PR #13).
6. Add `parse_known_arguments()` method on `Command`. Like `parse_arguments()`, but unrecognised options are collected into the result instead of raising an error. Access them via `result.get_unknown_args()`. Useful for forwarding unknown flags to another program (PR #13).
7. Add `.allow_hyphen_values()` builder method on `Argument`. When set on a positional, values starting with `-` are accepted without requiring `--` (e.g., `-` for stdin). Remainder positionals have this enabled automatically (PR #13).
8. **CJK-aware help alignment.** Help output now computes column padding using terminal display width instead of byte length. CJK ideographs and fullwidth characters are correctly treated as 2-column-wide, so help descriptions stay aligned when option names, positional names, or subcommand names contain Chinese, Japanese, or Korean characters. ANSI escape sequences are skipped during width calculation. No API changes — this is automatic (PR #14).

### 🔧 Fixes and API changes

Expand Down
72 changes: 52 additions & 20 deletions docs/user_manual.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ from argmojo import Argument, Command
- [Auto-generated Help](#auto-generated-help)
- [Custom Tips](#custom-tips)
- [Version Display](#version-display)
- [CJK-Aware Help Alignment](#cjk-aware-help-alignment)
- [Parsing Behaviour](#parsing-behaviour)
- [Negative Number Passthrough](#negative-number-passthrough)
- [Long Option Prefix Matching](#long-option-prefix-matching)
Expand Down Expand Up @@ -2207,9 +2208,7 @@ Options:
-V, --version Show version
```

Help text columns are **dynamically aligned**: the padding between the option
names and the description text adjusts automatically based on the longest
option line, so everything stays neatly aligned regardless of option length.
Help text columns are **dynamically aligned**: the padding between the option names and the description text adjusts automatically based on the longest option line, so everything stays neatly aligned regardless of option length.

---

Expand All @@ -2236,9 +2235,7 @@ var help_plain = command._generate_help(color=False) # no ANSI codes

**Custom Colours**

The **header colour**, **argument-name colour**, **deprecation warning
colour**, and **parse error colour** are all customisable. Section headers
always keep the **bold + underline** style; only the colour changes.
The **header colour**, **argument-name colour**, **deprecation warning colour**, and **parse error colour** are all customisable. Section headers always keep the **bold + underline** style; only the colour changes.

```mojo
var command = Command("myapp", "My app")
Expand All @@ -2264,9 +2261,7 @@ Available colour names (case-insensitive):

An unrecognised colour name raises an `Error` at runtime.

Padding calculation is always based on the **plain-text width** (without
escape codes), so columns remain correctly aligned regardless of whether
colour is enabled.
Padding calculation is always based on the **plain-text width** (without escape codes), so columns remain correctly aligned regardless of whether colour is enabled.

**What controls the output:**

Expand Down Expand Up @@ -2302,8 +2297,7 @@ This takes priority over the `color=True` default but does **not** override an e

**Show Help When No Arguments Provided**

Use `help_on_no_arguments()` to automatically display help when the user invokes
the command with no arguments (like `git`, `docker`, or `cargo`):
Use `help_on_no_arguments()` to automatically display help when the user invokes the command with no arguments (like `git`, `docker`, or `cargo`):

```mojo
var command = Command("myapp", "My application")
Expand All @@ -2317,14 +2311,11 @@ myapp # prints help and exits
myapp --file x # normal parsing
```

This is particularly useful for commands that require arguments — instead of
showing an obscure "missing required argument" error, the user sees the
full help text.
This is particularly useful for commands that require arguments — instead of showing an obscure "missing required argument" error, the user sees the full help text.

### Custom Tips

Add custom **tip lines** to the bottom of your help output with `add_tip()`.
This is useful for documenting common patterns, gotchas, or examples.
Add custom **tip lines** to the bottom of your help output with `add_tip()`. This is useful for documenting common patterns, gotchas, or examples.

```mojo
var command = Command("calc", "A calculator")
Expand Down Expand Up @@ -2352,10 +2343,7 @@ Tip: Use quotes if you use spaces in expressions.

---

**Smart default tip** — when positional arguments are defined, ArgMojo automatically adds a
built-in tip explaining the `--` separator. The example in this default tip adapts
based on whether negative numbers are auto-detected: if they are, it uses
`-my-value`; otherwise, it uses `-10.18`.
**Smart default tip** — when positional arguments are defined, ArgMojo automatically adds a built-in tip explaining the `--` separator. The example in this default tip adapts based on whether negative numbers are auto-detected: if they are, it uses `-my-value`; otherwise, it uses `-10.18`.

User-defined tips appear **below** the built-in tip.

Expand Down Expand Up @@ -2386,6 +2374,50 @@ var command = Command("myapp", "Description", version="1.0.0")

After printing the version, the program exits cleanly with exit code 0.

### CJK-Aware Help Alignment

ArgMojo automatically handles CJK (Chinese, Japanese, Korean) characters in help output. CJK ideographs and fullwidth characters occupy **two terminal columns** instead of one, so naïve byte- or codepoint-based padding would cause misaligned help columns.

ArgMojo's help formatter uses **display width** (East Asian Width) to compute padding, so help descriptions stay aligned even when option names, positional names, subcommand names, or help text contain CJK characters.

See [Unicode v17.0](https://www.unicode.org/charts/PDF/Unicode-17.0/) for details on CJK character ranges and properties.
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The manual links to “Unicode v17.0”, but the implementation comments reference Unicode 16.0 for the East Asian Width ranges. To avoid confusion (and potential stale/invalid links), consider aligning the referenced Unicode version across docs and code, or using a versionless link to the Unicode East Asian Width specification.

Suggested change
See [Unicode v17.0](https://www.unicode.org/charts/PDF/Unicode-17.0/) for details on CJK character ranges and properties.
See the [Unicode East Asian Width specification](https://www.unicode.org/reports/tr11/) for details on CJK character ranges and properties.

Copilot uses AI. Check for mistakes.

**Example — mixed ASCII and CJK options:**

```mojo
var command = Command("工具", "一個命令行工具")
command.add_argument(
Argument("output", help="Output path").long("output").short("o")
)
command.add_argument(
Argument("編碼", help="設定編碼").long("編碼")
)
```

```txt
Options:
-o, --output <output> Output path
--編碼 <編碼> 設定編碼
```

**Example — CJK subcommands:**

```mojo
var app = Command("工具", "一個命令行工具")
var init_cmd = Command("初始化", "建立新項目")
app.add_subcommand(init_cmd^)
var build_cmd = Command("構建", "編譯項目")
app.add_subcommand(build_cmd^)
```

```txt
Commands:
初始化 建立新項目
構建 編譯項目
```

No configuration is needed — CJK-aware alignment is always active.

## Parsing Behaviour

### Negative Number Passthrough
Expand Down
212 changes: 212 additions & 0 deletions examples/yu.mojo
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
"""Example: Yuhao Input Method character code lookup.

例:宇浩輸入法單字編碼查詢

A CJK-heavy demo that showcases ArgMojo's CJK-aware help alignment.
The purpose of the app is to lookup the encoding of Chinese characters in the
Yuhao Input Method (宇浩輸入法).

In Yuhao Input Method, each Chinese character is represented by a 4-letter code
based on its components and radicals. For example, the character "字" is encoded
as "khvi" in the Lingming variant.

Yuhao Input Method has several variants: The app supports looking up any variant
individually or all three side by side.

For full character tables, see https://shurufa.app

This demo app supports three Yuhao IME variants:
- 宇浩靈明 (--ling) — default
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example’s docstring lists a --ling option, but the CLI only defines --joy, --star, and --all (Lingming is implicit default). Either add an explicit --ling flag or adjust the docstring so the documented flags match the implemented arguments.

Suggested change
- 宇浩靈明 (--ling) — default
- 宇浩靈明 — default (used when no variant flag is given)

Copilot uses AI. Check for mistakes.
- 宇浩卿雲 (--joy)
- 宇浩星陳 (--star)

Try these (build first with: `pixi run build`):

./yu --help
./yu 字
./yu 宇浩靈明
./yu --joy 字根
./yu --star 你好
./yu --all 宇浩
./yu --version
"""

from argmojo import Argument, Command


fn _build_ling_table() -> Dict[String, String]:
"""Build 宇浩靈明 lookup table (20 high-frequency characters)."""
var d: Dict[String, String] = {
"的": "d",
"一": "fi",
"是": "i",
"不": "u",
"了": "a",
"人": "ne",
"我": "o",
"在": "mvu",
"有": "me",
"他": "jse",
"這": "rwo",
"個": "ju",
"上": "ka",
"來": "rla",
"到": "kva",
"大": "yda",
"中": "di",
"字": "khvi",
"宇": "kfjo",
"浩": "vmdo",
"你": "ja",
"好": "fhi",
}
return d^


fn _build_joy_table() -> Dict[String, String]:
"""Build 宇浩卿雲 lookup table (20 high-frequency characters)."""
var d: Dict[String, String] = {
"的": "d",
"一": "f",
"是": "j",
"不": "n",
"了": "l",
"人": "ur",
"我": "w",
"在": "xl",
"有": "x",
"他": "e",
"這": "ruc",
"個": "ebog",
"上": "o",
"來": "cl",
"到": "uo",
"大": "md",
"中": "k",
"字": "il",
"宇": "ife",
"浩": "npk",
"你": "eo",
"好": "wlz",
}
return d^


fn _build_star_table() -> Dict[String, String]:
"""Build 宇浩星陳 lookup table (20 high-frequency characters)."""
var d: Dict[String, String] = {
"的": "d",
"一": "f",
"是": "j",
"不": "v",
"了": "k",
"人": "r",
"我": "g",
"在": "eu",
"有": "ew",
"他": "eo",
"這": "bocy",
"個": "ewj",
"上": "jv",
"來": "all",
"到": "dm",
"大": "o",
"中": "l",
"字": "ikz",
"宇": "ifk",
"浩": "npl",
"你": "e",
"好": "c",
}
return d^


fn _lookup(table: Dict[String, String], ch: String) raises -> String:
if ch in table:
return table[ch]
return "(未收錄)"


fn main() raises:
var app = Command(
"yu",
"宇浩輸入法單字編碼查詢。完整碼表請見 https://shurufa.app",
version="0.1.0",
)

app.add_argument(
Argument("漢字", help="要查詢的漢字(可以輸入多個漢字)").positional().required()
)
app.add_argument(
Argument("joy", help="使用卿雲編碼(預設為靈明)").long("joy").short("j").flag()
)
app.add_argument(
Argument("star", help="使用星陳編碼(預設為靈明)").long("star").short("s").flag()
)
app.add_argument(
Argument("all", help="同時顯示靈明、卿雲、星陳編碼").long("all").short("a").flag()
)

app.add_tip("完整碼表與教程請訪問 https://shurufa.app")

var args = app.parse()
var input = args.get_string("漢字")
var use_joy = args.get_flag("joy")
var use_star = args.get_flag("star")
var show_all = args.get_flag("all")

var ling = _build_ling_table()
var joy = _build_joy_table()
var star = _build_star_table()

# Extract individual codepoints from the UTF-8 input string.
var chars = List[String]()
var bytes = input.as_bytes()
var i = 0
var n = len(bytes)
while i < n:
var b0 = Int(bytes[i])
var seq_len: Int
if b0 < 0x80:
seq_len = 1
elif b0 < 0xE0:
seq_len = 2
elif b0 < 0xF0:
seq_len = 3
else:
seq_len = 4
chars.append(String(input[i : i + seq_len]))
i += seq_len

if show_all:
print("漢字\t靈明\t卿雲\t星陳")
print("────\t────\t────\t────")
for k in range(len(chars)):
var ch = chars[k]
print(
ch
+ "\t"
+ _lookup(ling, ch)
+ "\t"
+ _lookup(joy, ch)
+ "\t"
+ _lookup(star, ch)
)
elif use_star:
print("漢字\t星陳編碼")
print("────\t────────")
for k in range(len(chars)):
var ch = chars[k]
print(ch + "\t" + _lookup(star, ch))
elif use_joy:
print("漢字\t卿雲編碼")
print("────\t────────")
for k in range(len(chars)):
var ch = chars[k]
print(ch + "\t" + _lookup(joy, ch))
else:
print("漢字\t靈明編碼")
print("────\t────────")
for k in range(len(chars)):
var ch = chars[k]
print(ch + "\t" + _lookup(ling, ch))
4 changes: 3 additions & 1 deletion pixi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ test = """\
build = """pixi run package \
&& mojo build -I src examples/mgrep.mojo -o mgrep \
&& mojo build -I src examples/mgit.mojo -o mgit \
&& mojo build -I src examples/demo.mojo -o demo"""
&& mojo build -I src examples/demo.mojo -o demo \
&& mojo build -I src examples/yu.mojo -o yu \
"""

# clean build artifacts
clean = "rm -f argmojo.mojopkg mgrep mgit demo"
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The build task now produces a yu binary, but the clean task still only removes mgrep, mgit, and demo. Consider updating clean to remove yu as well so pixi run clean stays accurate.

Suggested change
clean = "rm -f argmojo.mojopkg mgrep mgit demo"
clean = "rm -f argmojo.mojopkg mgrep mgit demo yu"

Copilot uses AI. Check for mistakes.
Loading