aboutsummaryrefslogtreecommitdiffhomepage
path: root/pkgs/nvim-setcellwidths-table-for-udev-gothic/main.py
blob: 483d8263d66c7cdd6cc49a22d235a069c3c6c540 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
import argparse
from collections.abc import Generator
from fontTools.ttLib import TTFont
from pathlib import Path
import unicodedata

def ambiguous_codepoints() -> Generator[int]:
    for cp in range(0x00000, 0x30000):
        try:
            char = chr(cp)
            eaw = unicodedata.east_asian_width(char)
            if eaw == 'A':
                yield cp
        except (ValueError, UnicodeEncodeError):
            continue

def get_glyph_width(font: TTFont, codepoint: int) -> int | None:
    cmap = font.getBestCmap()
    if cmap is None:
        return None

    glyph_name = cmap.get(codepoint)
    if glyph_name is None:
        return None

    if 'hmtx' not in font:
        return None

    hmtx = font['hmtx']
    if glyph_name not in hmtx.metrics:
        return None

    width, _ = hmtx.metrics[glyph_name]
    return width

def get_reference_widths(font: TTFont) -> tuple[int | None, int | None]:
    halfwidth_ref = None
    for cp in [0x0041, 0x0030, 0x0061]:
        w = get_glyph_width(font, cp)
        if w is not None and w > 0:
            halfwidth_ref = w
            break

    fullwidth_ref = None
    for cp in [0xFF21, 0x3042, 0x6F22, 0x4E00]:
        w = get_glyph_width(font, cp)
        if w is not None and w > 0:
            fullwidth_ref = w
            break

    return halfwidth_ref, fullwidth_ref

def classify_width(width: int, half_ref: int, full_ref: int) -> str:
    if width <= (half_ref + full_ref) / 2:
        return "half"
    else:
        return "full"

def analyze_font(font_path: str) -> dict:
    font = TTFont(font_path)

    stats = {
        "half": [],
        "full": [],
    }

    half_ref, full_ref = get_reference_widths(font)
    if half_ref is None or full_ref is None:
        return stats

    for cp in ambiguous_codepoints():
        width = get_glyph_width(font, cp)
        if width is None:
            continue

        classification = classify_width(width, half_ref, full_ref)
        stats[classification].append(cp)

    font.close()
    return stats

def merge_ranges(codepoints: list[int]) -> list[tuple[int, int]]:
    if not codepoints:
        return []
    sorted_cps = sorted(codepoints)
    ranges = []
    start = sorted_cps[0]
    end = sorted_cps[0]
    for cp in sorted_cps[1:]:
        if cp == end + 1:
            end = cp
        else:
            ranges.append((start, end))
            start = cp
            end = cp
    ranges.append((start, end))
    return ranges

def generate_table(stats: dict) -> str:
    prefix = [
        "local M = {}",
        "",
        "function M.setcellwidths()",
        "   vim.fn.setcellwidths({",
    ]
    table = []
    for start, end in merge_ranges(stats["full"]):
        chars = ", ".join("%s (U+%04X)" % (chr(cp), cp) for cp in range(start, end + 1))
        table.append("      -- %s" % chars)
        table.append("      {%#06x, %#06x, 2}," % (start, end))
    suffix = [
        "   })",
        "end",
        "",
        "return M",
    ]
    return "\n".join(prefix + table + suffix)

def main():
    parser = argparse.ArgumentParser(
        description='Generate Neovim setcellwidths() table'
    )
    parser.add_argument('font', help='Font file path')

    args = parser.parse_args()

    if not Path(args.font).exists():
        print(f"Font file not found: {args.font}")
        return 1

    stats = analyze_font(args.font)
    table = generate_table(stats)
    print(table)
    return 0

if __name__ == '__main__':
    exit(main())