vim2bat 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vim2bat/__init__.py +0 -0
- vim2bat/__main__.py +5 -0
- vim2bat/cli.py +761 -0
- vim2bat/share/loose.tsv +84 -0
- vim2bat/share/strict.tsv +37 -0
- vim2bat/share/vim-groups.vim +58 -0
- vim2bat/share/vim-slots.vim +80 -0
- vim2bat-0.1.0.dist-info/METADATA +231 -0
- vim2bat-0.1.0.dist-info/RECORD +12 -0
- vim2bat-0.1.0.dist-info/WHEEL +4 -0
- vim2bat-0.1.0.dist-info/entry_points.txt +2 -0
- vim2bat-0.1.0.dist-info/licenses/LICENSE +21 -0
vim2bat/__init__.py
ADDED
|
File without changes
|
vim2bat/__main__.py
ADDED
vim2bat/cli.py
ADDED
|
@@ -0,0 +1,761 @@
|
|
|
1
|
+
"""Builds bat themes from Vim colorschemes.
|
|
2
|
+
|
|
3
|
+
vim2bat runs Vim with your vimrc, records the colors and attributes a
|
|
4
|
+
colorscheme gives each highlight group (Table 1), and writes bat themes that
|
|
5
|
+
map TextMate scopes to them: share/strict.tsv (Table 2) for Vim's standard
|
|
6
|
+
groups, and share/loose.tsv (Table 3) for language-specific groups. vim2bat
|
|
7
|
+
compare measures a theme against Vim with share/vim-slots.vim.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import fcntl
|
|
12
|
+
import os
|
|
13
|
+
import plistlib
|
|
14
|
+
import re
|
|
15
|
+
import select
|
|
16
|
+
import shlex
|
|
17
|
+
import shutil
|
|
18
|
+
import signal
|
|
19
|
+
import subprocess
|
|
20
|
+
import sys
|
|
21
|
+
import tempfile
|
|
22
|
+
import threading
|
|
23
|
+
from collections import Counter
|
|
24
|
+
from collections.abc import Iterator, Sequence
|
|
25
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
26
|
+
from contextlib import contextmanager, suppress
|
|
27
|
+
from importlib.metadata import version
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from types import FrameType
|
|
30
|
+
from typing import IO, NamedTuple, NoReturn, NotRequired, TypedDict
|
|
31
|
+
from xml.parsers.expat import ExpatError
|
|
32
|
+
|
|
33
|
+
SHARE = Path(__file__).with_name("share")
|
|
34
|
+
BACKGROUNDS = ("dark", "light")
|
|
35
|
+
# A scheme that resets 'background' to Vim's guess, such as default, gets the
|
|
36
|
+
# guess from the background color in COLORFGBG.
|
|
37
|
+
COLORFGBG = {"dark": "15;0", "light": "0;15"}
|
|
38
|
+
# bat themes support only these attributes.
|
|
39
|
+
ATTRIBUTES = ("bold", "italic", "underline")
|
|
40
|
+
SGR = re.compile(r"\x1b\[([0-9;]*)m")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Directories(NamedTuple):
|
|
44
|
+
themes: Path
|
|
45
|
+
cache: Path
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Rule(TypedDict):
|
|
49
|
+
# A theme's first rule, which holds its global colors, has no name or scope.
|
|
50
|
+
name: NotRequired[str]
|
|
51
|
+
scope: NotRequired[str]
|
|
52
|
+
settings: dict[str, str]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Theme(TypedDict):
|
|
56
|
+
name: str
|
|
57
|
+
comment: str
|
|
58
|
+
settings: list[Rule]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class Failure(Exception):
|
|
62
|
+
"""Stops vim2bat with exit status 1, after printing the message if any."""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class Stop(BaseException):
|
|
66
|
+
"""Raised by a signal handler, so that vim2bat can stop Vim before exiting."""
|
|
67
|
+
|
|
68
|
+
def __init__(self, signum: int) -> None:
|
|
69
|
+
super().__init__(signum)
|
|
70
|
+
self.signum = signum
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# Vim processes still running, which vim2bat kills when it is stopped.
|
|
74
|
+
_running: set[subprocess.Popen[bytes]] = set()
|
|
75
|
+
_running_lock = threading.Lock()
|
|
76
|
+
_stopping = threading.Event()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@contextmanager
|
|
80
|
+
def vim_error_file() -> Iterator[IO[bytes]]:
|
|
81
|
+
"""Yields a temporary file to give Vim as its stderr."""
|
|
82
|
+
with tempfile.TemporaryFile() as errors:
|
|
83
|
+
# Vim writes some messages, such as a vimrc's errors, to its stderr
|
|
84
|
+
# directly. On Linux, the Vim scripts' writes through /dev/stderr keep
|
|
85
|
+
# their own offset, so without O_APPEND each would overwrite the other.
|
|
86
|
+
flags = fcntl.fcntl(errors, fcntl.F_GETFL)
|
|
87
|
+
fcntl.fcntl(errors, fcntl.F_SETFL, flags | os.O_APPEND)
|
|
88
|
+
yield errors
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def run_vim(
|
|
92
|
+
arguments: list[str], environment: dict[str, str], stderr: IO[bytes] | None = None
|
|
93
|
+
) -> int:
|
|
94
|
+
"""Runs Vim with your vimrc on a pseudo-terminal and returns its status.
|
|
95
|
+
|
|
96
|
+
Vim without a terminal numbers named cterm colors (such as DarkBlue)
|
|
97
|
+
differently from a terminal, so it gets one, as xterm-256color. What Vim
|
|
98
|
+
draws there is discarded, including most error messages, so the Vim
|
|
99
|
+
scripts write their errors to stderr.
|
|
100
|
+
"""
|
|
101
|
+
if stderr is None:
|
|
102
|
+
# On Linux, /dev/stderr opens a redirected stderr's file anew, so
|
|
103
|
+
# vim2bat's own writes to a file opened with 2> would overwrite Vim's.
|
|
104
|
+
with vim_error_file() as errors:
|
|
105
|
+
try:
|
|
106
|
+
return run_vim(arguments, environment, errors)
|
|
107
|
+
finally:
|
|
108
|
+
errors.seek(0)
|
|
109
|
+
sys.stderr.write(errors.read().decode("utf-8", "replace"))
|
|
110
|
+
if _stopping.is_set():
|
|
111
|
+
raise Failure("")
|
|
112
|
+
controller, terminal = os.openpty()
|
|
113
|
+
# -n skips the swap file, whose warning for a file open in another Vim
|
|
114
|
+
# would count as an error. nomore keeps Vim from pausing at -- More --
|
|
115
|
+
# after a screenful of messages, which the Enter below would page through
|
|
116
|
+
# one line at a time.
|
|
117
|
+
command = [
|
|
118
|
+
"vim",
|
|
119
|
+
"-N",
|
|
120
|
+
"-i",
|
|
121
|
+
"NONE",
|
|
122
|
+
"-n",
|
|
123
|
+
"--cmd",
|
|
124
|
+
"set nomore",
|
|
125
|
+
*arguments,
|
|
126
|
+
"-c",
|
|
127
|
+
"qa!",
|
|
128
|
+
]
|
|
129
|
+
# Its own session keeps Ctrl-C from interrupting Vim's script, which would
|
|
130
|
+
# leave Vim waiting for input instead of reaching qa!. vim2bat stops it
|
|
131
|
+
# instead.
|
|
132
|
+
try:
|
|
133
|
+
process = subprocess.Popen(
|
|
134
|
+
command,
|
|
135
|
+
stdin=terminal,
|
|
136
|
+
stdout=terminal,
|
|
137
|
+
stderr=stderr,
|
|
138
|
+
env={**os.environ, "TERM": "xterm-256color", **environment},
|
|
139
|
+
start_new_session=True,
|
|
140
|
+
)
|
|
141
|
+
except BaseException:
|
|
142
|
+
os.close(controller)
|
|
143
|
+
raise
|
|
144
|
+
finally:
|
|
145
|
+
os.close(terminal)
|
|
146
|
+
with _running_lock:
|
|
147
|
+
_running.add(process)
|
|
148
|
+
if _stopping.is_set():
|
|
149
|
+
process.kill()
|
|
150
|
+
try:
|
|
151
|
+
# A command the vimrc started in the background with :! can hold the
|
|
152
|
+
# terminal open after Vim exits.
|
|
153
|
+
while process.poll() is None:
|
|
154
|
+
if not select.select([controller], [], [], 0.5)[0]:
|
|
155
|
+
# Vim waits for a key at a prompt, such as the one after an
|
|
156
|
+
# error message, so an idle half second gets an Enter. If Vim
|
|
157
|
+
# was busy instead, it never reads the Enter, because it quits
|
|
158
|
+
# at the -c 'qa!' that ends its arguments.
|
|
159
|
+
try:
|
|
160
|
+
os.write(controller, b"\r")
|
|
161
|
+
except OSError:
|
|
162
|
+
break
|
|
163
|
+
continue
|
|
164
|
+
try:
|
|
165
|
+
if not os.read(controller, 65536):
|
|
166
|
+
break
|
|
167
|
+
# Some systems report the closed terminal as an error rather than
|
|
168
|
+
# end of file.
|
|
169
|
+
except OSError:
|
|
170
|
+
break
|
|
171
|
+
return process.wait()
|
|
172
|
+
except BaseException:
|
|
173
|
+
process.kill()
|
|
174
|
+
raise
|
|
175
|
+
finally:
|
|
176
|
+
os.close(controller)
|
|
177
|
+
with _running_lock:
|
|
178
|
+
_running.discard(process)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def stop_vims() -> None:
|
|
182
|
+
with _running_lock:
|
|
183
|
+
_stopping.set()
|
|
184
|
+
for process in _running:
|
|
185
|
+
process.kill()
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def bat_config_dir() -> Path:
|
|
189
|
+
result = subprocess.run(
|
|
190
|
+
["bat", "--config-dir"], capture_output=True, text=True, check=True
|
|
191
|
+
)
|
|
192
|
+
folder = result.stdout.strip()
|
|
193
|
+
if not os.path.isabs(folder):
|
|
194
|
+
raise Failure(
|
|
195
|
+
f"bat's config folder {folder!r} isn't an absolute path; check "
|
|
196
|
+
"BAT_CONFIG_DIR"
|
|
197
|
+
)
|
|
198
|
+
return Path(folder)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def cache_dir() -> Path:
|
|
202
|
+
# The XDG Base Directory spec says to ignore a relative path.
|
|
203
|
+
base = os.environ.get("XDG_CACHE_HOME", "")
|
|
204
|
+
return (Path(base) if os.path.isabs(base) else Path.home() / ".cache") / "vim2bat"
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def ask_vim(expression: str) -> list[str]:
|
|
208
|
+
"""Returns the lines of a list or string that Vim evaluates with your vimrc."""
|
|
209
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
210
|
+
out = Path(directory) / "out"
|
|
211
|
+
status = run_vim(
|
|
212
|
+
["-c", f"call writefile(flatten([{expression}]), $OUTFILE)"],
|
|
213
|
+
{"OUTFILE": str(out)},
|
|
214
|
+
)
|
|
215
|
+
if status != 0 or not out.exists():
|
|
216
|
+
raise Failure(f"Vim couldn't evaluate {expression}")
|
|
217
|
+
return out.read_text(encoding="utf-8").splitlines()
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def color(value: str) -> str:
|
|
221
|
+
"""Returns a Table 1 color as a bat theme color.
|
|
222
|
+
|
|
223
|
+
bat reads a color whose alpha is 00 as a palette slot number, and one whose
|
|
224
|
+
alpha is 01 as the terminal's default. Table 1 holds #rrggbb colors under
|
|
225
|
+
'termguicolors'.
|
|
226
|
+
"""
|
|
227
|
+
if not value:
|
|
228
|
+
return "#00000001"
|
|
229
|
+
if value.startswith("#"):
|
|
230
|
+
return value
|
|
231
|
+
return f"#{int(value):02X}000000"
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def read_table(path: Path) -> dict[str, tuple[str, str, str]]:
|
|
235
|
+
"""Returns Table 1 as {group: (foreground, background, font style)}."""
|
|
236
|
+
groups = {}
|
|
237
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
238
|
+
group, foreground, background, attributes = line.split("\t")
|
|
239
|
+
style = " ".join(a for a in attributes.split(",") if a in ATTRIBUTES)
|
|
240
|
+
groups[group] = (foreground, background, style)
|
|
241
|
+
return groups
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def read_rules(strict: bool) -> list[tuple[str, str, bool]]:
|
|
245
|
+
"""Returns (group, scopes, loose) for Table 2 and, unless strict, Table 3."""
|
|
246
|
+
rows = [(group, scopes, False) for group, scopes in tsv(SHARE / "strict.tsv", 2)]
|
|
247
|
+
if not strict:
|
|
248
|
+
rows += [
|
|
249
|
+
(group, scopes, True)
|
|
250
|
+
for _filetype, group, scopes in tsv(SHARE / "loose.tsv", 3)
|
|
251
|
+
]
|
|
252
|
+
return rows
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def tsv(path: Path, fields: int) -> list[list[str]]:
|
|
256
|
+
return [
|
|
257
|
+
line.split("\t", fields - 1)
|
|
258
|
+
for line in path.read_text(encoding="utf-8").splitlines()
|
|
259
|
+
if line
|
|
260
|
+
]
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def rule(
|
|
264
|
+
groups: dict[str, tuple[str, str, str]], group: str, scopes: str, loose: bool
|
|
265
|
+
) -> Rule:
|
|
266
|
+
foreground, _background, style = groups[group]
|
|
267
|
+
# bat keeps the color of a less specific rule that also matches when this
|
|
268
|
+
# one has no foreground, so a loose rule always writes one, taking Normal's
|
|
269
|
+
# when its group has none.
|
|
270
|
+
if loose and not foreground:
|
|
271
|
+
foreground = groups["Normal"][0]
|
|
272
|
+
settings = {}
|
|
273
|
+
if foreground or loose:
|
|
274
|
+
settings["foreground"] = color(foreground)
|
|
275
|
+
# Written even when empty, so that a rule nested inside another doesn't
|
|
276
|
+
# inherit the outer rule's bold or italic.
|
|
277
|
+
settings["fontStyle"] = style
|
|
278
|
+
return {"name": group, "scope": scopes, "settings": settings}
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def theme(
|
|
282
|
+
name: str,
|
|
283
|
+
comment: str,
|
|
284
|
+
groups: dict[str, tuple[str, str, str]],
|
|
285
|
+
rules: list[tuple[str, str, bool]],
|
|
286
|
+
) -> Theme:
|
|
287
|
+
globals_ = {
|
|
288
|
+
"foreground": color(groups["Normal"][0]),
|
|
289
|
+
# bat's --highlight-line, which fzf.vim's preview uses to mark the
|
|
290
|
+
# matched line, draws with lineHighlight.
|
|
291
|
+
"lineHighlight": color(groups["CursorLine"][1]),
|
|
292
|
+
}
|
|
293
|
+
# A group from a newer Vim can't color anything in this one, so it gets no
|
|
294
|
+
# rule.
|
|
295
|
+
scoped = [rule(groups, *row) for row in rules if row[0] in groups]
|
|
296
|
+
return {
|
|
297
|
+
"name": name,
|
|
298
|
+
# vim2bat compare reads the options a theme was built with from here.
|
|
299
|
+
"comment": comment,
|
|
300
|
+
"settings": [{"settings": globals_}, *scoped],
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def theme_names(scheme: str, backgrounds: Sequence[str]) -> dict[str, str]:
|
|
305
|
+
"""Names the themes: the background is part of the name only when the
|
|
306
|
+
scheme has both."""
|
|
307
|
+
if len(backgrounds) > 1:
|
|
308
|
+
return {background: f"vim-{scheme}-{background}" for background in backgrounds}
|
|
309
|
+
return {background: f"vim-{scheme}" for background in backgrounds}
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def build_comment(scheme: str, options: argparse.Namespace) -> str:
|
|
313
|
+
words = ["vim2bat"]
|
|
314
|
+
if options.strict:
|
|
315
|
+
words.append("-s")
|
|
316
|
+
if options.truecolor:
|
|
317
|
+
words.append("-t")
|
|
318
|
+
if options.palette:
|
|
319
|
+
words += ["-p", options.palette]
|
|
320
|
+
if scheme.startswith("-"):
|
|
321
|
+
words.append("--")
|
|
322
|
+
return shlex.join([*words, scheme])
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def vim_environment(
|
|
326
|
+
scheme: str, background: str, options: argparse.Namespace
|
|
327
|
+
) -> dict[str, str]:
|
|
328
|
+
return {
|
|
329
|
+
"SCHEME": scheme,
|
|
330
|
+
"BACKGROUND": background,
|
|
331
|
+
"COLORFGBG": COLORFGBG[background],
|
|
332
|
+
"T_CO": options.palette or "",
|
|
333
|
+
"TERMGUICOLORS": "1" if options.truecolor else "",
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def build(
|
|
338
|
+
scheme: str,
|
|
339
|
+
options: argparse.Namespace,
|
|
340
|
+
directories: Directories,
|
|
341
|
+
stderr: IO[bytes] | None = None,
|
|
342
|
+
) -> None:
|
|
343
|
+
"""Writes the themes for one scheme."""
|
|
344
|
+
tables = directories.cache / "tables" / scheme
|
|
345
|
+
tables.mkdir(parents=True, exist_ok=True)
|
|
346
|
+
found = []
|
|
347
|
+
for background in BACKGROUNDS:
|
|
348
|
+
table = tables / f"vim-groups-{background}.tsv"
|
|
349
|
+
environment = {
|
|
350
|
+
**vim_environment(scheme, background, options),
|
|
351
|
+
"OUTFILE": str(table),
|
|
352
|
+
}
|
|
353
|
+
table.unlink(missing_ok=True)
|
|
354
|
+
# vim-groups.vim prints why it failed.
|
|
355
|
+
status = run_vim(["-S", str(SHARE / "vim-groups.vim")], environment, stderr)
|
|
356
|
+
if status != 0:
|
|
357
|
+
raise Failure(f"Vim exited with status {status}")
|
|
358
|
+
if not table.exists():
|
|
359
|
+
raise Failure(f"Vim wrote no table for {scheme}")
|
|
360
|
+
# vim-groups.vim writes an empty table for a background the scheme lacks.
|
|
361
|
+
if table.stat().st_size:
|
|
362
|
+
found.append(background)
|
|
363
|
+
rules = read_rules(options.strict)
|
|
364
|
+
comment = build_comment(scheme, options)
|
|
365
|
+
directories.themes.mkdir(parents=True, exist_ok=True)
|
|
366
|
+
for background, name in theme_names(scheme, found).items():
|
|
367
|
+
groups = read_table(tables / f"vim-groups-{background}.tsv")
|
|
368
|
+
# Two schemes can build the same name in parallel, such as one with both
|
|
369
|
+
# backgrounds and another with its name plus -dark, so each write gets
|
|
370
|
+
# its own temporary file.
|
|
371
|
+
descriptor, temporary = tempfile.mkstemp(dir=directories.themes, suffix=".tmp")
|
|
372
|
+
try:
|
|
373
|
+
with os.fdopen(descriptor, "wb") as f:
|
|
374
|
+
plistlib.dump(theme(name, comment, groups, rules), f, sort_keys=False)
|
|
375
|
+
os.chmod(temporary, 0o644)
|
|
376
|
+
os.replace(temporary, directories.themes / f"{name}.tmTheme")
|
|
377
|
+
except BaseException:
|
|
378
|
+
Path(temporary).unlink(missing_ok=True)
|
|
379
|
+
raise
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def build_quietly(
|
|
383
|
+
scheme: str, options: argparse.Namespace, directories: Directories
|
|
384
|
+
) -> tuple[bool, str]:
|
|
385
|
+
"""Builds one scheme for --all and returns (succeeded, its error output)."""
|
|
386
|
+
with vim_error_file() as errors:
|
|
387
|
+
message = ""
|
|
388
|
+
try:
|
|
389
|
+
build(scheme, options, directories, errors)
|
|
390
|
+
succeeded = True
|
|
391
|
+
except Failure as failure:
|
|
392
|
+
succeeded = False
|
|
393
|
+
if str(failure):
|
|
394
|
+
message = f"vim2bat: {failure}\n"
|
|
395
|
+
errors.seek(0)
|
|
396
|
+
return succeeded, errors.read().decode("utf-8", "replace") + message
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def build_all(options: argparse.Namespace, directories: Directories) -> list[str]:
|
|
400
|
+
"""Builds every scheme Vim lists, one per CPU at a time, and returns the
|
|
401
|
+
ones that failed."""
|
|
402
|
+
schemes = ask_vim('getcompletion("", "color")')
|
|
403
|
+
failed = []
|
|
404
|
+
with ThreadPoolExecutor(os.cpu_count() or 1) as pool:
|
|
405
|
+
try:
|
|
406
|
+
futures = {
|
|
407
|
+
pool.submit(build_quietly, scheme, options, directories): scheme
|
|
408
|
+
for scheme in schemes
|
|
409
|
+
}
|
|
410
|
+
for count, future in enumerate(as_completed(futures), 1):
|
|
411
|
+
scheme = futures[future]
|
|
412
|
+
succeeded, errors = future.result()
|
|
413
|
+
# Each scheme's errors follow its progress line, so that errors
|
|
414
|
+
# from schemes building at the same time don't interleave.
|
|
415
|
+
print(f"vim2bat: [{count}/{len(schemes)}] {scheme}", file=sys.stderr)
|
|
416
|
+
sys.stderr.write(errors)
|
|
417
|
+
if not succeeded:
|
|
418
|
+
failed.append(scheme)
|
|
419
|
+
except BaseException:
|
|
420
|
+
pool.shutdown(wait=False, cancel_futures=True)
|
|
421
|
+
stop_vims()
|
|
422
|
+
raise
|
|
423
|
+
return failed
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def build_cache() -> None:
|
|
427
|
+
# bat reads cache as a file name instead of its subcommand when the current
|
|
428
|
+
# folder has an entry named cache.
|
|
429
|
+
with tempfile.TemporaryDirectory() as empty:
|
|
430
|
+
result = subprocess.run(
|
|
431
|
+
["bat", "cache", "--build"],
|
|
432
|
+
cwd=empty,
|
|
433
|
+
capture_output=True,
|
|
434
|
+
text=True,
|
|
435
|
+
check=True,
|
|
436
|
+
)
|
|
437
|
+
# bat reports a theme it can't parse on stdout and still exits 0, so only
|
|
438
|
+
# the lines a clean build prints are dropped.
|
|
439
|
+
for line in result.stdout.splitlines():
|
|
440
|
+
if not line.endswith(" ... okay") and not line.startswith(
|
|
441
|
+
"No syntaxes were found"
|
|
442
|
+
):
|
|
443
|
+
print(line, file=sys.stderr)
|
|
444
|
+
sys.stderr.write(result.stderr)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def generate_parser(
|
|
448
|
+
parser_class: type[argparse.ArgumentParser] = argparse.ArgumentParser,
|
|
449
|
+
) -> argparse.ArgumentParser:
|
|
450
|
+
parser = parser_class(
|
|
451
|
+
prog="vim2bat",
|
|
452
|
+
description=(
|
|
453
|
+
"Builds bat themes from a Vim colorscheme, as Vim shows it with your "
|
|
454
|
+
"vimrc in a 256-color terminal. A scheme with both backgrounds gets "
|
|
455
|
+
"vim-SCHEME-dark and vim-SCHEME-light; one with a single background "
|
|
456
|
+
"gets vim-SCHEME. Then it rebuilds bat's cache."
|
|
457
|
+
),
|
|
458
|
+
epilog=(
|
|
459
|
+
"To measure a theme against Vim, run vim2bat compare THEME FILE... "
|
|
460
|
+
"(see vim2bat compare --help). To build a scheme named compare, run "
|
|
461
|
+
"vim2bat -- compare."
|
|
462
|
+
),
|
|
463
|
+
)
|
|
464
|
+
target = parser.add_mutually_exclusive_group(required=True)
|
|
465
|
+
target.add_argument("scheme", nargs="?", help="the colorscheme to build")
|
|
466
|
+
target.add_argument(
|
|
467
|
+
"-a", "--all", action="store_true", help="build every colorscheme Vim lists"
|
|
468
|
+
)
|
|
469
|
+
parser.add_argument(
|
|
470
|
+
"-c",
|
|
471
|
+
"--clean",
|
|
472
|
+
action="store_true",
|
|
473
|
+
help="first delete vim2bat's cache and the whole bat themes folder",
|
|
474
|
+
)
|
|
475
|
+
parser.add_argument(
|
|
476
|
+
"-s",
|
|
477
|
+
"--strict",
|
|
478
|
+
action="store_true",
|
|
479
|
+
help="map only Vim's standard groups (strict.tsv)",
|
|
480
|
+
)
|
|
481
|
+
parser.add_argument(
|
|
482
|
+
"-t",
|
|
483
|
+
"--truecolor",
|
|
484
|
+
action="store_true",
|
|
485
|
+
help="turn on 'termguicolors' to use the scheme's 24-bit colors",
|
|
486
|
+
)
|
|
487
|
+
parser.add_argument(
|
|
488
|
+
"-p",
|
|
489
|
+
"--palette",
|
|
490
|
+
choices=("16", "256"),
|
|
491
|
+
help="override your vimrc's 't_Co' with 16 or 256 colors",
|
|
492
|
+
)
|
|
493
|
+
parser.add_argument(
|
|
494
|
+
"--version", action="version", version=f"%(prog)s {version('vim2bat')}"
|
|
495
|
+
)
|
|
496
|
+
return parser
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def generate(argv: list[str]) -> None:
|
|
500
|
+
parser = generate_parser()
|
|
501
|
+
options = parser.parse_args(argv)
|
|
502
|
+
# vim-groups.vim passes the name to :colorscheme, which reads a blank name,
|
|
503
|
+
# or one that starts with | or ", as no name and keeps the vimrc's scheme.
|
|
504
|
+
if options.scheme is not None and re.match(r'\s*($|["|])', options.scheme):
|
|
505
|
+
parser.error("the scheme name is empty")
|
|
506
|
+
# The scheme name becomes part of a theme's file name.
|
|
507
|
+
if "/" in (options.scheme or ""):
|
|
508
|
+
parser.error("a scheme name can't contain /")
|
|
509
|
+
directories = Directories(bat_config_dir() / "themes", cache_dir())
|
|
510
|
+
if options.clean:
|
|
511
|
+
folders = (directories.cache, directories.themes)
|
|
512
|
+
for folder in folders:
|
|
513
|
+
if folder.is_symlink():
|
|
514
|
+
raise Failure(
|
|
515
|
+
f"{folder} is a symbolic link; -c deletes only a real folder"
|
|
516
|
+
)
|
|
517
|
+
for folder in folders:
|
|
518
|
+
with suppress(FileNotFoundError):
|
|
519
|
+
shutil.rmtree(folder)
|
|
520
|
+
if options.all:
|
|
521
|
+
failed = build_all(options, directories)
|
|
522
|
+
# This runs even when some schemes failed, so that the rest are usable.
|
|
523
|
+
build_cache()
|
|
524
|
+
if failed:
|
|
525
|
+
raise Failure(f"failed: {' '.join(failed)}")
|
|
526
|
+
return
|
|
527
|
+
build(options.scheme, options, directories)
|
|
528
|
+
build_cache()
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def compare_parser() -> argparse.ArgumentParser:
|
|
532
|
+
parser = argparse.ArgumentParser(
|
|
533
|
+
prog="vim2bat compare",
|
|
534
|
+
description=(
|
|
535
|
+
"Reports how often a bat theme that vim2bat built shows each character "
|
|
536
|
+
"of FILE in the same color as Vim does, with the options the theme was "
|
|
537
|
+
"built with. To measure strict.tsv alone, build the theme with -s first, "
|
|
538
|
+
"since both builds use the same name."
|
|
539
|
+
),
|
|
540
|
+
)
|
|
541
|
+
parser.add_argument("theme", metavar="THEME")
|
|
542
|
+
parser.add_argument("files", metavar="FILE", nargs="+")
|
|
543
|
+
return parser
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def bat_colors(text: str) -> dict[tuple[int, int], str | None]:
|
|
547
|
+
"""Returns {(line, index): color} for each character of bat's output."""
|
|
548
|
+
colors = {}
|
|
549
|
+
for lnum, line in enumerate(text.split("\n"), 1):
|
|
550
|
+
current, idx = None, 0
|
|
551
|
+
for k, part in enumerate(SGR.split(line)):
|
|
552
|
+
if k % 2:
|
|
553
|
+
current = apply(current, part)
|
|
554
|
+
continue
|
|
555
|
+
for _ in part:
|
|
556
|
+
colors[(lnum, idx)] = current
|
|
557
|
+
idx += 1
|
|
558
|
+
return colors
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def apply(current: str | None, params: str) -> str | None:
|
|
562
|
+
"""Returns the foreground color after one SGR sequence."""
|
|
563
|
+
codes = params.split(";") if params else ["0"]
|
|
564
|
+
k = 0
|
|
565
|
+
while k < len(codes):
|
|
566
|
+
code = codes[k]
|
|
567
|
+
if code == "38" and codes[k + 1] == "2":
|
|
568
|
+
current = "#" + "".join(f"{int(c):02x}" for c in codes[k + 2 : k + 5])
|
|
569
|
+
k += 5
|
|
570
|
+
continue
|
|
571
|
+
if code == "38":
|
|
572
|
+
current = codes[k + 2]
|
|
573
|
+
k += 3
|
|
574
|
+
continue
|
|
575
|
+
if code in ("0", "39"):
|
|
576
|
+
current = None
|
|
577
|
+
elif code.isdigit() and 30 <= int(code) <= 37:
|
|
578
|
+
current = str(int(code) - 30)
|
|
579
|
+
k += 1
|
|
580
|
+
return current
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
class CommentParser(argparse.ArgumentParser):
|
|
584
|
+
def error(self, message: str) -> NoReturn:
|
|
585
|
+
raise ValueError(message)
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def built_with(theme_name: str, comment: str) -> argparse.Namespace:
|
|
589
|
+
"""Returns the options in a theme's comment, if vim2bat built the theme
|
|
590
|
+
under its own name."""
|
|
591
|
+
options: argparse.Namespace | None
|
|
592
|
+
try:
|
|
593
|
+
words = shlex.split(comment)
|
|
594
|
+
if words[:1] != ["vim2bat"]:
|
|
595
|
+
raise ValueError
|
|
596
|
+
options = generate_parser(CommentParser).parse_args(words[1:])
|
|
597
|
+
except ValueError:
|
|
598
|
+
options = None
|
|
599
|
+
if (
|
|
600
|
+
options is None
|
|
601
|
+
or options.scheme is None
|
|
602
|
+
or theme_name
|
|
603
|
+
not in (
|
|
604
|
+
f"vim-{options.scheme}",
|
|
605
|
+
*theme_names(options.scheme, BACKGROUNDS).values(),
|
|
606
|
+
)
|
|
607
|
+
):
|
|
608
|
+
raise Failure(f"{theme_name} wasn't built by vim2bat under that name")
|
|
609
|
+
return options
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def compare(argv: list[str]) -> None:
|
|
613
|
+
args = compare_parser().parse_args(argv)
|
|
614
|
+
# bat falls back to its default theme for an unknown name instead of failing.
|
|
615
|
+
listed = subprocess.run(
|
|
616
|
+
["bat", "--list-themes", "--color=never"],
|
|
617
|
+
capture_output=True,
|
|
618
|
+
text=True,
|
|
619
|
+
check=True,
|
|
620
|
+
).stdout.splitlines()
|
|
621
|
+
if args.theme not in listed:
|
|
622
|
+
raise Failure(f"no bat theme named {args.theme}")
|
|
623
|
+
path = bat_config_dir() / "themes" / f"{args.theme}.tmTheme"
|
|
624
|
+
if not path.exists():
|
|
625
|
+
raise Failure(
|
|
626
|
+
f"{path} doesn't exist; vim2bat compare measures only themes that "
|
|
627
|
+
"vim2bat built"
|
|
628
|
+
)
|
|
629
|
+
try:
|
|
630
|
+
data = plistlib.loads(path.read_bytes())
|
|
631
|
+
except (ValueError, ExpatError) as error:
|
|
632
|
+
raise Failure(f"can't read {path}: {error}") from None
|
|
633
|
+
comment = data.get("comment") if isinstance(data, dict) else None
|
|
634
|
+
options = built_with(args.theme, comment if isinstance(comment, str) else "")
|
|
635
|
+
scheme = options.scheme
|
|
636
|
+
suffix = args.theme[len(f"vim-{scheme}") :]
|
|
637
|
+
# A scheme with one background sets 'background' itself.
|
|
638
|
+
background = suffix[1:] if suffix else "dark"
|
|
639
|
+
totals: Counter[str] = Counter()
|
|
640
|
+
agrees: Counter[str] = Counter()
|
|
641
|
+
mismatches: Counter[tuple[str, str, str, str | None]] = Counter()
|
|
642
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
643
|
+
for index, name in enumerate(args.files):
|
|
644
|
+
out = Path(directory) / f"{index}.tsv"
|
|
645
|
+
environment = {
|
|
646
|
+
**vim_environment(scheme, background, options),
|
|
647
|
+
"TARGET": os.path.join(os.getcwd(), name),
|
|
648
|
+
"OUTFILE": str(out),
|
|
649
|
+
}
|
|
650
|
+
# The backtracking regex engine, which macOS's Vim uses by default,
|
|
651
|
+
# stalls on the TypeScript syntax. Other syntaxes keep the default
|
|
652
|
+
# because the NFA engine highlights some of them, such as ERB,
|
|
653
|
+
# differently. The autocmd gets its own group, which a bare
|
|
654
|
+
# autocmd! in your vimrc doesn't clear.
|
|
655
|
+
status = run_vim(
|
|
656
|
+
[
|
|
657
|
+
"--cmd",
|
|
658
|
+
"augroup vim2bat | augroup END",
|
|
659
|
+
"--cmd",
|
|
660
|
+
(
|
|
661
|
+
"autocmd vim2bat FileType typescript,typescriptreact "
|
|
662
|
+
"set regexpengine=2"
|
|
663
|
+
),
|
|
664
|
+
"-S",
|
|
665
|
+
str(SHARE / "vim-slots.vim"),
|
|
666
|
+
],
|
|
667
|
+
environment,
|
|
668
|
+
)
|
|
669
|
+
if status != 0:
|
|
670
|
+
print(f"vim2bat: Vim reported an error on {name}", file=sys.stderr)
|
|
671
|
+
# vim-slots.vim writes nothing when the scheme fails to load.
|
|
672
|
+
if not out.exists():
|
|
673
|
+
raise Failure("")
|
|
674
|
+
# Without COLORTERM, bat rounds #rrggbb colors to the nearest of 256.
|
|
675
|
+
bat = subprocess.run(
|
|
676
|
+
[
|
|
677
|
+
"bat",
|
|
678
|
+
f"--theme={args.theme}",
|
|
679
|
+
"--color=always",
|
|
680
|
+
"--style=plain",
|
|
681
|
+
"--tabs=0",
|
|
682
|
+
"--wrap=never",
|
|
683
|
+
"--",
|
|
684
|
+
name,
|
|
685
|
+
],
|
|
686
|
+
capture_output=True,
|
|
687
|
+
check=True,
|
|
688
|
+
env={**os.environ, "COLORTERM": "truecolor"},
|
|
689
|
+
)
|
|
690
|
+
got = bat_colors(bat.stdout.decode("utf-8"))
|
|
691
|
+
lines = out.read_text(encoding="utf-8").splitlines()
|
|
692
|
+
lang = lines[0].removeprefix("#ft=") if lines else ""
|
|
693
|
+
total = agree = 0
|
|
694
|
+
for row in lines[1:]:
|
|
695
|
+
lnum, idx, vim, chain = row.split("\t")
|
|
696
|
+
bat_color = got.get((int(lnum), int(idx)))
|
|
697
|
+
total += 1
|
|
698
|
+
# Vim has no color for text in a Normal without a foreground,
|
|
699
|
+
# which bat draws in the terminal's default.
|
|
700
|
+
if bat_color == (vim or None):
|
|
701
|
+
agree += 1
|
|
702
|
+
else:
|
|
703
|
+
mismatches[(lang, chain or "(no syntax item)", vim, bat_color)] += 1
|
|
704
|
+
print(f"{100 * agree / max(total, 1):5.1f}% {total:6} {lang:16} {name}")
|
|
705
|
+
for key in (lang, "all"):
|
|
706
|
+
totals[key] += total
|
|
707
|
+
agrees[key] += agree
|
|
708
|
+
print()
|
|
709
|
+
for key in sorted(totals, key=lambda key: key == "all"):
|
|
710
|
+
print(
|
|
711
|
+
f"{100 * agrees[key] / max(totals[key], 1):5.1f}% {totals[key]:6} {key}"
|
|
712
|
+
)
|
|
713
|
+
print("\nchars language Vim syntax item>links Vim color bat color")
|
|
714
|
+
for (lang, chain, vim, bat_color), n in mismatches.most_common(30):
|
|
715
|
+
print(
|
|
716
|
+
f"{n:5} {lang} {chain} {vim or 'default'} "
|
|
717
|
+
f"{'default' if bat_color is None else bat_color}"
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
def stop(signum: int, _frame: FrameType | None) -> NoReturn:
|
|
722
|
+
raise Stop(signum)
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
def main(argv: list[str] | None = None) -> int:
|
|
726
|
+
argv = sys.argv[1:] if argv is None else argv
|
|
727
|
+
for signum in (signal.SIGINT, signal.SIGTERM, signal.SIGHUP):
|
|
728
|
+
signal.signal(signum, stop)
|
|
729
|
+
try:
|
|
730
|
+
if argv[:1] == ["compare"]:
|
|
731
|
+
compare(argv[1:])
|
|
732
|
+
else:
|
|
733
|
+
generate(argv)
|
|
734
|
+
except Stop as stopped:
|
|
735
|
+
stop_vims()
|
|
736
|
+
# A shell running vim2bat in a loop stops the loop on Ctrl-C only when
|
|
737
|
+
# vim2bat dies by the signal, not when it exits with 130. Dying skips the
|
|
738
|
+
# flush of stdout and stderr that exiting does.
|
|
739
|
+
sys.stdout.flush()
|
|
740
|
+
sys.stderr.flush()
|
|
741
|
+
signal.signal(stopped.signum, signal.SIG_DFL)
|
|
742
|
+
os.kill(os.getpid(), stopped.signum)
|
|
743
|
+
return 128 + stopped.signum
|
|
744
|
+
except Failure as failure:
|
|
745
|
+
if str(failure):
|
|
746
|
+
print(f"vim2bat: {failure}", file=sys.stderr)
|
|
747
|
+
return 1
|
|
748
|
+
except FileNotFoundError as error:
|
|
749
|
+
print(f"vim2bat: {error.filename} not found", file=sys.stderr)
|
|
750
|
+
return 1
|
|
751
|
+
except OSError as error:
|
|
752
|
+
print(f"vim2bat: {error}", file=sys.stderr)
|
|
753
|
+
return 1
|
|
754
|
+
except subprocess.CalledProcessError as error:
|
|
755
|
+
print(f"vim2bat: {shlex.join(error.cmd)} failed", file=sys.stderr)
|
|
756
|
+
reason = (
|
|
757
|
+
error.stderr.decode() if isinstance(error.stderr, bytes) else error.stderr
|
|
758
|
+
)
|
|
759
|
+
sys.stderr.write(SGR.sub("", reason or ""))
|
|
760
|
+
return 1
|
|
761
|
+
return 0
|
vim2bat/share/loose.tsv
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
ruby Normal punctuation.accessor.dot.ruby, punctuation.accessor.double-colon.ruby, punctuation.definition.group.begin.ruby, punctuation.definition.group.end.ruby, punctuation.section.array.ruby, punctuation.section.scope.ruby, punctuation.separator.sequence.ruby, punctuation.separator.ruby, punctuation.separator.key-value.ruby, punctuation.separator.inheritance.ruby, punctuation.definition.parameters.begin.ruby, punctuation.definition.parameters.end.ruby, punctuation.terminator.statement.ruby
|
|
2
|
+
ruby Normal punctuation.definition.constant.ruby
|
|
3
|
+
ruby Normal keyword.operator.arithmetic.ruby, keyword.operator.assignment.ruby, keyword.operator.assignment.augmented.ruby, keyword.operator.bitwise.ruby, keyword.operator.comparison.ruby, keyword.operator.conditional.ruby, keyword.operator.logical.ruby, keyword.operator.other.ruby, keyword.operator.range.ruby, keyword.operator.ruby, keyword.operator.splat.ruby
|
|
4
|
+
ruby Normal support.function.builtin.ruby
|
|
5
|
+
ruby Normal variable.parameter.ruby
|
|
6
|
+
ruby rubySymbol constant.other.symbol.ruby, constant.other.symbol.single-quoted.ruby, constant.other.symbol.double-quoted.ruby
|
|
7
|
+
ruby rubyConstant support.class.ruby, variable.other.constant.ruby, entity.name.constant.ruby, entity.other.inherited-class.ruby, support.other.namespace.ruby
|
|
8
|
+
ruby rubyClassName entity.name.class.ruby
|
|
9
|
+
ruby rubyModuleName entity.name.namespace.ruby
|
|
10
|
+
ruby rubyDefine keyword.declaration.function.ruby
|
|
11
|
+
ruby rubyClass keyword.declaration.class.ruby
|
|
12
|
+
ruby rubyModule keyword.declaration.namespace.ruby
|
|
13
|
+
ruby rubyMacro keyword.other.special-method.ruby
|
|
14
|
+
ruby rubyInclude keyword.control.import.ruby
|
|
15
|
+
ruby rubyPseudoVariable variable.language.ruby
|
|
16
|
+
ruby rubyInstanceVariable punctuation.definition.variable.ruby
|
|
17
|
+
ruby rubyComment punctuation.definition.comment.ruby
|
|
18
|
+
ruby rubyFloat punctuation.separator.decimal.ruby
|
|
19
|
+
ruby rubyDocumentation punctuation.definition.comment.begin.ruby, punctuation.definition.comment.end.ruby
|
|
20
|
+
ruby rubyRegexpDelimiter keyword.other.ruby
|
|
21
|
+
ruby rubyInteger punctuation.definition.numeric.base.ruby
|
|
22
|
+
html htmlTagName entity.name.tag.structure.any.html, entity.name.tag.block.any.html, entity.name.tag.block.form.html, entity.name.tag.inline.any.html, entity.name.tag.inline.form.html, entity.name.tag.inline.a.html, entity.name.tag.inline.table.html
|
|
23
|
+
html htmlArg entity.other.attribute-name.html, entity.other.attribute-name.class.html, entity.other.attribute-name.id.html, entity.other.attribute-name.style.html
|
|
24
|
+
html htmlTag punctuation.definition.tag.begin.html, punctuation.definition.tag.end.html
|
|
25
|
+
html htmlString punctuation.definition.string.begin.html, punctuation.definition.string.end.html
|
|
26
|
+
css cssClassName source.css entity.other.attribute-name.class.css
|
|
27
|
+
css cssBraces source.css punctuation.section.property-list.css
|
|
28
|
+
css cssUnitDecorators source.css keyword.other.unit.css
|
|
29
|
+
css cssNoise source.css punctuation.separator.key-value.css, source.css punctuation.terminator.rule.css
|
|
30
|
+
css cssTagName source.css entity.name.tag.css
|
|
31
|
+
css cssClassNameDot source.css punctuation.definition.entity.css
|
|
32
|
+
css cssImportant source.css keyword.other.important.css
|
|
33
|
+
typescript Normal source.ts variable.other, source.tsx variable.other, variable.other.constant.ts, variable.other.constant.tsx, variable.other.constant.object.ts, variable.other.constant.object.tsx, variable.other.constant.property.ts, variable.other.constant.property.tsx, variable.other.constant.object.property.ts, variable.other.constant.object.property.tsx
|
|
34
|
+
typescript Normal source.ts punctuation.separator.comma, source.tsx punctuation.separator.comma, source.ts punctuation.accessor, source.tsx punctuation.accessor, source.ts punctuation.separator.key-value, source.tsx punctuation.separator.key-value, source.ts punctuation.terminator.statement, source.tsx punctuation.terminator.statement
|
|
35
|
+
typescript typescriptBraces source.ts punctuation.definition.block, source.tsx punctuation.definition.block
|
|
36
|
+
typescript Normal entity.name.function.ts, entity.name.function.tsx
|
|
37
|
+
typescript typescriptObjectLabel meta.object-literal.key.ts, meta.object-literal.key.tsx
|
|
38
|
+
typescript typescriptTypeReference entity.name.type.ts, entity.name.type.tsx
|
|
39
|
+
typescript Keyword support.variable.property.dom.ts, support.variable.property.dom.tsx
|
|
40
|
+
typescript typescriptCall variable.parameter.ts, variable.parameter.tsx
|
|
41
|
+
typescript typescriptVariable storage.type.ts, storage.type.tsx
|
|
42
|
+
typescript typescriptString punctuation.definition.string.begin.ts, punctuation.definition.string.begin.tsx, punctuation.definition.string.end.ts, punctuation.definition.string.end.tsx
|
|
43
|
+
typescriptreact tsxTagName support.class.component.tsx
|
|
44
|
+
typescriptreact tsxAttrib entity.other.attribute-name.tsx
|
|
45
|
+
typescriptreact tsxTag punctuation.definition.tag.begin.tsx, punctuation.definition.tag.end.tsx
|
|
46
|
+
javascript Normal variable.other.readwrite.js, variable.other.object.js, variable.other.property.js, variable.other.property.static.js, variable.other.class.js, variable.other.constant.js, variable.parameter.function.js, variable.function.js, variable.function.constructor.js, variable.function.tagged-template.js, entity.name.function.js
|
|
47
|
+
javascript Normal string.unquoted.label.js
|
|
48
|
+
javascript Normal keyword.operator.accessor.js, keyword.operator.assignment.js, keyword.operator.assignment.augmented.js, keyword.operator.arithmetic.js, keyword.operator.comparison.js, keyword.operator.relational.js, keyword.operator.logical.js, keyword.operator.bitwise.js, keyword.operator.ternary.js, keyword.operator.spread.js, storage.type.function.arrow.js
|
|
49
|
+
javascript Normal punctuation.terminator.statement.js, punctuation.separator.key-value.js, punctuation.separator.parameter.function.js
|
|
50
|
+
javascriptreact Normal punctuation.definition.tag.begin.jsx, punctuation.definition.tag.end.jsx, keyword.operator.assignment.jsx
|
|
51
|
+
javascript javaScriptReserved storage.type.js, storage.type.class.js, storage.type.extends.js
|
|
52
|
+
javascript javaScriptFunction storage.type.function.js
|
|
53
|
+
javascript javaScriptBraces meta.brace.curly.begin.js, meta.brace.curly.end.js, meta.brace.square.begin.js, meta.brace.square.end.js, punctuation.section.embedded.begin.jsx, punctuation.section.embedded.end.jsx
|
|
54
|
+
javascript String punctuation.definition.string.begin.js, punctuation.definition.string.end.js, punctuation.definition.string.begin.jsx, punctuation.definition.string.end.jsx
|
|
55
|
+
scss Normal source.scss variable.function, source.scss entity.name.function
|
|
56
|
+
scss PreProc source.scss keyword.control.at-rule
|
|
57
|
+
sass sassClass source.scss entity.other.attribute-name.class
|
|
58
|
+
css cssTagName source.scss entity.name.tag
|
|
59
|
+
css cssPseudoClassId source.scss entity.other.pseudo-class, source.scss entity.other.pseudo-element
|
|
60
|
+
scss Normal source.scss punctuation.terminator.rule
|
|
61
|
+
scss Normal source.scss punctuation.separator.key-value
|
|
62
|
+
scss Normal source.scss punctuation.separator.sequence
|
|
63
|
+
scss Normal source.scss punctuation.definition.group
|
|
64
|
+
css cssBraces source.scss punctuation.section.property-list
|
|
65
|
+
sass sassVariable source.scss punctuation.definition.variable
|
|
66
|
+
yaml yamlPlainScalar string.unquoted.plain.out.yaml
|
|
67
|
+
yaml yamlBlockString string.unquoted.block.yaml
|
|
68
|
+
yaml yamlBlockMappingKey entity.name.tag.yaml
|
|
69
|
+
yaml yamlAlias variable.other.alias.yaml
|
|
70
|
+
vim vimFunctionBang meta.function.viml
|
|
71
|
+
vim vimCommand storage.function.viml
|
|
72
|
+
vim vimFunctionName entity.name.function.viml
|
|
73
|
+
vim vimCommand keyword.control.viml
|
|
74
|
+
vim vimCommand support.function.viml
|
|
75
|
+
vim Special support.type.viml
|
|
76
|
+
gitcommit gitcommitSummary markup.heading.subject.git.commit
|
|
77
|
+
gitcommit gitcommitComment punctuation.definition.comment.git.commit
|
|
78
|
+
gitcommit gitcommitSelectedFile markup.changed.file.content.git.commit
|
|
79
|
+
diff diffLine meta.range.unified.diff, punctuation.definition.range.diff
|
|
80
|
+
diff diffSubname entity.name.section.diff
|
|
81
|
+
diff diffOldFile meta.header.from-file.diff, punctuation.definition.from-file.diff
|
|
82
|
+
diff diffNewFile meta.header.to-file.diff, punctuation.definition.to-file.diff
|
|
83
|
+
diff diffAdded punctuation.definition.inserted.diff
|
|
84
|
+
diff diffRemoved punctuation.definition.deleted.diff
|
vim2bat/share/strict.tsv
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
Comment comment, punctuation.definition.comment
|
|
2
|
+
Constant constant, entity.name.constant, variable.other.constant, support.constant
|
|
3
|
+
String string
|
|
4
|
+
Character constant.character
|
|
5
|
+
Number constant.numeric
|
|
6
|
+
Boolean constant.language.boolean
|
|
7
|
+
Float constant.numeric.float
|
|
8
|
+
Identifier variable, support.variable
|
|
9
|
+
Function entity.name.function, variable.function, support.function
|
|
10
|
+
Statement keyword
|
|
11
|
+
Conditional keyword.control.conditional, keyword.control.switch
|
|
12
|
+
Repeat keyword.control.loop
|
|
13
|
+
Label keyword.control.conditional.case, keyword.control.conditional.default, entity.name.label, variable.label
|
|
14
|
+
Operator keyword.operator
|
|
15
|
+
Keyword keyword.other
|
|
16
|
+
Exception keyword.control.exception, keyword.control.trycatch, keyword.control.flow.throw, keyword.control.flow.raise
|
|
17
|
+
PreProc keyword.control.preprocessor, keyword.other.preprocessor
|
|
18
|
+
Include keyword.control.import.include
|
|
19
|
+
Define keyword.control.import.define
|
|
20
|
+
Macro entity.name.macro, support.macro, variable.macro, entity.name.constant.preprocessor, entity.name.function.preprocessor, variable.function.assumed-macro
|
|
21
|
+
PreCondit keyword.control.import.else, meta.preprocessor keyword.control.import
|
|
22
|
+
Type storage.type, entity.name.type, support.type
|
|
23
|
+
StorageClass storage.modifier
|
|
24
|
+
Structure storage.type.struct, storage.type.union, storage.type.enum, storage.type.class, keyword.declaration.struct, keyword.declaration.union, keyword.declaration.enum, keyword.declaration.class, storage.type.keyword.struct
|
|
25
|
+
Typedef entity.name.type.typedef
|
|
26
|
+
SpecialChar constant.character.escape
|
|
27
|
+
Delimiter punctuation
|
|
28
|
+
SpecialComment keyword.other.documentation
|
|
29
|
+
Underlined markup.underline
|
|
30
|
+
Bold markup.bold
|
|
31
|
+
Italic markup.italic
|
|
32
|
+
BoldItalic markup.bold markup.italic, markup.italic markup.bold
|
|
33
|
+
Error invalid
|
|
34
|
+
Todo comment.line.todo, keyword.other.todo
|
|
35
|
+
Added markup.inserted
|
|
36
|
+
Changed markup.changed
|
|
37
|
+
Removed markup.deleted
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
" Writes $OUTFILE: every highlight group with the foreground color, background
|
|
2
|
+
" color (palette slots, or #rrggbb under 'termguicolors') and attributes that
|
|
3
|
+
" the colorscheme $SCHEME gives it when 'background' is $BACKGROUND (dark or
|
|
4
|
+
" light), or no groups if $SCHEME lacks that background.
|
|
5
|
+
|
|
6
|
+
" vim2bat's -p and -t options, which override your vimrc.
|
|
7
|
+
if $T_CO !=# ''
|
|
8
|
+
let &t_Co = $T_CO
|
|
9
|
+
endif
|
|
10
|
+
if $TERMGUICOLORS !=# ''
|
|
11
|
+
set termguicolors
|
|
12
|
+
endif
|
|
13
|
+
let &background = $BACKGROUND
|
|
14
|
+
" The standard groups, and the links of the language groups, exist only with
|
|
15
|
+
" syntax highlighting on, which a vimrc can leave off.
|
|
16
|
+
if !exists('g:syntax_on')
|
|
17
|
+
syntax enable
|
|
18
|
+
endif
|
|
19
|
+
|
|
20
|
+
" Vim on a terminal exits 0 after an error, so any error here makes it exit 1
|
|
21
|
+
" instead, but not one that silent! hides. That includes a scheme that fails to
|
|
22
|
+
" load, which would leave the colors of whatever scheme your vimrc loaded.
|
|
23
|
+
try
|
|
24
|
+
execute 'colorscheme' $SCHEME
|
|
25
|
+
|
|
26
|
+
let s:dir = expand('<sfile>:p:h')
|
|
27
|
+
" Language groups exist only once their syntax file has run. They load after
|
|
28
|
+
" the colorscheme because its :hi clear hides them from getcompletion().
|
|
29
|
+
for s:ft in uniq(sort(map(readfile(s:dir . '/loose.tsv'), 'split(v:val, "\t")[0]')))
|
|
30
|
+
let &syntax = s:ft
|
|
31
|
+
endfor
|
|
32
|
+
|
|
33
|
+
let s:attrs = ['bold', 'italic', 'underline', 'undercurl', 'strikethrough', 'reverse', 'standout']
|
|
34
|
+
let s:rows = []
|
|
35
|
+
for s:group in filter(getcompletion('', 'highlight'), 'hlexists(v:val)')
|
|
36
|
+
let s:id = synIDtrans(hlID(s:group))
|
|
37
|
+
let s:on = []
|
|
38
|
+
for s:attr in s:attrs
|
|
39
|
+
if synIDattr(s:id, s:attr, 'cterm') == '1'
|
|
40
|
+
call add(s:on, s:attr)
|
|
41
|
+
endif
|
|
42
|
+
endfor
|
|
43
|
+
" With 'termguicolors', Vim draws a group's GUI colors, or its cterm colors
|
|
44
|
+
" if it has none, and always its cterm attributes.
|
|
45
|
+
let s:colors = &termguicolors ? [synIDattr(s:id, 'fg#', 'gui'), synIDattr(s:id, 'bg#', 'gui')] : ['', '']
|
|
46
|
+
if s:colors == ['', '']
|
|
47
|
+
let s:colors = [synIDattr(s:id, 'fg', 'cterm'), synIDattr(s:id, 'bg', 'cterm')]
|
|
48
|
+
endif
|
|
49
|
+
call add(s:rows, join([s:group] + s:colors + [join(s:on, ',')], "\t"))
|
|
50
|
+
endfor
|
|
51
|
+
" A scheme with one background switches 'background' to it.
|
|
52
|
+
call writefile(&background ==# $BACKGROUND ? s:rows : [], $OUTFILE)
|
|
53
|
+
catch
|
|
54
|
+
" On Linux, /dev/stderr opens a redirected stderr's file anew, which Vim
|
|
55
|
+
" would truncate without 'a'.
|
|
56
|
+
call writefile([v:exception], '/dev/stderr', 'a')
|
|
57
|
+
cquit
|
|
58
|
+
endtry
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
" Writes $OUTFILE: for each non-blank character of $TARGET, its line, its
|
|
2
|
+
" character index, the color (palette slot, or #rrggbb under 'termguicolors')
|
|
3
|
+
" the colorscheme $SCHEME shows it in after 'background' is set to $BACKGROUND,
|
|
4
|
+
" and the chain of links from its syntax item to the group that colors it. The
|
|
5
|
+
" first line is the filetype.
|
|
6
|
+
|
|
7
|
+
" See vim-groups.vim.
|
|
8
|
+
if $T_CO !=# ''
|
|
9
|
+
let &t_Co = $T_CO
|
|
10
|
+
endif
|
|
11
|
+
if $TERMGUICOLORS !=# ''
|
|
12
|
+
set termguicolors
|
|
13
|
+
endif
|
|
14
|
+
let &background = $BACKGROUND
|
|
15
|
+
if !exists('g:syntax_on')
|
|
16
|
+
syntax enable
|
|
17
|
+
endif
|
|
18
|
+
try | execute 'colorscheme' $SCHEME | catch | call writefile([v:exception], '/dev/stderr', 'a') | cquit | endtry
|
|
19
|
+
|
|
20
|
+
let s:chains = {}
|
|
21
|
+
function s:Chain(name) abort
|
|
22
|
+
if !has_key(s:chains, a:name)
|
|
23
|
+
let l:chain = [a:name]
|
|
24
|
+
while 1
|
|
25
|
+
let l:next = matchstr(execute('highlight ' . l:chain[-1]), 'links to \zs\S\+')
|
|
26
|
+
if l:next ==# '' || index(l:chain, l:next, 0, v:true) >= 0
|
|
27
|
+
break
|
|
28
|
+
endif
|
|
29
|
+
call add(l:chain, l:next)
|
|
30
|
+
endwhile
|
|
31
|
+
let s:chains[a:name] = join(l:chain, '>')
|
|
32
|
+
endif
|
|
33
|
+
return s:chains[a:name]
|
|
34
|
+
endfunction
|
|
35
|
+
|
|
36
|
+
" As in vim-groups.vim.
|
|
37
|
+
let s:fgs = {}
|
|
38
|
+
function s:Fg(id) abort
|
|
39
|
+
if !has_key(s:fgs, a:id)
|
|
40
|
+
let l:colors = &termguicolors ? [synIDattr(a:id, 'fg#', 'gui'), synIDattr(a:id, 'bg#', 'gui')] : ['', '']
|
|
41
|
+
let s:fgs[a:id] = l:colors == ['', ''] ? synIDattr(a:id, 'fg', 'cterm') : l:colors[0]
|
|
42
|
+
endif
|
|
43
|
+
return s:fgs[a:id]
|
|
44
|
+
endfunction
|
|
45
|
+
|
|
46
|
+
let s:rows = []
|
|
47
|
+
" As in vim-groups.vim, but $OUTFILE is still written after an error, so that
|
|
48
|
+
" vim2bat compare compares what there is and reports the error.
|
|
49
|
+
try
|
|
50
|
+
execute 'edit' fnameescape($TARGET)
|
|
51
|
+
" Highlight each line as Vim would after scrolling down from the top.
|
|
52
|
+
syntax sync fromstart
|
|
53
|
+
call add(s:rows, '#ft=' . &filetype)
|
|
54
|
+
let s:normal = s:Fg(synIDtrans(hlID('Normal')))
|
|
55
|
+
for s:lnum in range(1, line('$'))
|
|
56
|
+
let s:col = 1
|
|
57
|
+
let s:idx = 0
|
|
58
|
+
for s:char in split(getline(s:lnum), '\zs')
|
|
59
|
+
" synID() misses a match that starts on a blank unless it visits that
|
|
60
|
+
" column too.
|
|
61
|
+
let s:id = synID(s:lnum, s:col, 1)
|
|
62
|
+
if s:char !~# '\s'
|
|
63
|
+
let s:fg = s:Fg(synIDtrans(s:id))
|
|
64
|
+
let s:name = synIDattr(s:id, 'name')
|
|
65
|
+
call add(s:rows, join([s:lnum, s:idx, s:fg ==# '' ? s:normal : s:fg, s:name ==# '' ? '' : s:Chain(s:name)], "\t"))
|
|
66
|
+
endif
|
|
67
|
+
let s:col += len(s:char)
|
|
68
|
+
" split() keeps composing characters with their base character. Count
|
|
69
|
+
" each one, as vim2bat compare does.
|
|
70
|
+
let s:idx += strchars(s:char)
|
|
71
|
+
endfor
|
|
72
|
+
endfor
|
|
73
|
+
catch
|
|
74
|
+
call writefile([v:exception], '/dev/stderr', 'a')
|
|
75
|
+
let s:failed = 1
|
|
76
|
+
endtry
|
|
77
|
+
call writefile(s:rows, $OUTFILE)
|
|
78
|
+
if exists('s:failed')
|
|
79
|
+
cquit
|
|
80
|
+
endif
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: vim2bat
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Generate bat themes from Vim colorschemes
|
|
5
|
+
Project-URL: Homepage, https://github.com/redodson01/vim2bat
|
|
6
|
+
Project-URL: Repository, https://github.com/redodson01/vim2bat
|
|
7
|
+
Project-URL: Issues, https://github.com/redodson01/vim2bat/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/redodson01/vim2bat/blob/main/CHANGELOG.md
|
|
9
|
+
Author: Richard E. Dodson
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: MacOS
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
23
|
+
Classifier: Topic :: Text Editors
|
|
24
|
+
Classifier: Topic :: Utilities
|
|
25
|
+
Requires-Python: >=3.11
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# vim2bat
|
|
29
|
+
|
|
30
|
+
[](https://github.com/redodson01/vim2bat/actions/workflows/ci.yml)
|
|
31
|
+
[](https://github.com/redodson01/vim2bat/releases/latest)
|
|
32
|
+
[](https://github.com/redodson01/vim2bat/releases/latest)
|
|
33
|
+
[](https://github.com/redodson01/vim2bat/blob/main/LICENSE)
|
|
34
|
+
|
|
35
|
+
Builds [bat](https://github.com/sharkdp/bat) themes from Vim colorschemes, so
|
|
36
|
+
that bat, and tools that use it such as fzf.vim's previews and delta, color code
|
|
37
|
+
the way Vim does.
|
|
38
|
+
|
|
39
|
+
## The problem
|
|
40
|
+
|
|
41
|
+
bat ships its own themes, and most of them use 24-bit colors. If you use a Vim
|
|
42
|
+
colorscheme, especially one built on your terminal's palette like solarized8
|
|
43
|
+
with `g:solarized_use16`, the previews and diffs that bat colors look different
|
|
44
|
+
from the code you edit in Vim. A matching bat theme means mapping Vim's
|
|
45
|
+
highlight groups to TextMate scopes by hand, for each scheme and background.
|
|
46
|
+
|
|
47
|
+
vim2bat asks Vim which colors a colorscheme gives each highlight group, with
|
|
48
|
+
your vimrc, and writes bat themes that use the same palette slots, or the same
|
|
49
|
+
24-bit colors.
|
|
50
|
+
|
|
51
|
+
## Install
|
|
52
|
+
|
|
53
|
+
### Homebrew (macOS and Linux)
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
brew install redodson01/tap/vim2bat
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### PyPI
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
uv tool install vim2bat # or: pipx install vim2bat
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### From source
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
git clone https://github.com/redodson01/vim2bat.git
|
|
69
|
+
cd vim2bat
|
|
70
|
+
uv tool install . # or: uv tool install --editable . to follow edits to the checkout
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The PyPI and source installs put the `vim2bat` command in `~/.local/bin` by
|
|
74
|
+
default. Make sure that folder is on your `$PATH` (`uv tool update-shell` or
|
|
75
|
+
`pipx ensurepath` adds it).
|
|
76
|
+
|
|
77
|
+
## Runtime requirements
|
|
78
|
+
|
|
79
|
+
- Vim 9.1 or newer, with the colorschemes you want to convert
|
|
80
|
+
- bat (0.25 or newer for the `--theme-dark` and `--theme-light` settings below)
|
|
81
|
+
- Python 3.11 or newer
|
|
82
|
+
|
|
83
|
+
Debian and Ubuntu install bat as `batcat`. vim2bat runs `bat`, so link it:
|
|
84
|
+
`ln -s /usr/bin/batcat ~/.local/bin/bat`.
|
|
85
|
+
|
|
86
|
+
## Usage
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
vim2bat retrobox # builds vim-retrobox-dark and vim-retrobox-light
|
|
90
|
+
vim2bat --all # builds every colorscheme Vim lists
|
|
91
|
+
bat --theme=vim-retrobox-dark file.rb
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
| Option | Effect |
|
|
95
|
+
|---|---|
|
|
96
|
+
| `-a`, `--all` | Builds every colorscheme Vim lists, several at a time |
|
|
97
|
+
| `-c`, `--clean` | First deletes vim2bat's cache and the whole bat themes folder |
|
|
98
|
+
| `-s`, `--strict` | Maps only Vim's standard highlight groups, such as `Comment` and `String` |
|
|
99
|
+
| `-t`, `--truecolor` | Turns on `termguicolors`, so the themes use the scheme's 24-bit colors |
|
|
100
|
+
| `-p`, `--palette 16\|256` | Overrides your vimrc's `t_Co` |
|
|
101
|
+
|
|
102
|
+
vim2bat rebuilds bat's cache when it finishes, so the themes are available right
|
|
103
|
+
away.
|
|
104
|
+
|
|
105
|
+
### Theme names
|
|
106
|
+
|
|
107
|
+
A scheme that supports both backgrounds gets `vim-SCHEME-dark` and
|
|
108
|
+
`vim-SCHEME-light`. A scheme that supports a single background, by setting
|
|
109
|
+
`background` itself, gets `vim-SCHEME`.
|
|
110
|
+
|
|
111
|
+
### What decides the colors
|
|
112
|
+
|
|
113
|
+
Vim runs with your vimrc in a 256-color terminal, so your vimrc decides what the
|
|
114
|
+
themes look like: a scheme's own options (such as `g:solarized_use16`), `t_Co`
|
|
115
|
+
and `termguicolors` all apply. `-p` and `-t` override `t_Co` and
|
|
116
|
+
`termguicolors` for one run. Each theme records the options it was built with in
|
|
117
|
+
its `comment` entry. `-p` has no effect while `termguicolors` is on, whether
|
|
118
|
+
from `-t` or your vimrc.
|
|
119
|
+
|
|
120
|
+
With `-t`, bat prints the exact colors only when `COLORTERM` is `truecolor` or
|
|
121
|
+
`24bit`. Otherwise it rounds them to the nearest of 256.
|
|
122
|
+
|
|
123
|
+
## Using the themes
|
|
124
|
+
|
|
125
|
+
### bat
|
|
126
|
+
|
|
127
|
+
Add these lines to bat's config file, whose path `bat --config-file` prints:
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
--theme-dark="vim-retrobox-dark"
|
|
131
|
+
--theme-light="vim-retrobox-light"
|
|
132
|
+
--italic-text=always
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
bat's default, `--theme=auto`, picks between them by asking the terminal for its
|
|
136
|
+
background. It doesn't ask when its output goes to a pipe, as in fzf.vim's
|
|
137
|
+
previews, and then uses its own default theme. On macOS, `--theme="auto:system"`
|
|
138
|
+
follows the system appearance instead, and works in pipes too.
|
|
139
|
+
|
|
140
|
+
bat leaves out italics unless `--italic-text` is `always`.
|
|
141
|
+
|
|
142
|
+
### delta
|
|
143
|
+
|
|
144
|
+
delta takes a single syntax theme:
|
|
145
|
+
|
|
146
|
+
```ini
|
|
147
|
+
[delta]
|
|
148
|
+
syntax-theme = vim-retrobox-dark
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
On macOS, git's pager command can pick the theme from the system appearance:
|
|
152
|
+
|
|
153
|
+
```ini
|
|
154
|
+
[core]
|
|
155
|
+
pager = delta --syntax-theme=vim-retrobox-$(defaults read -g AppleInterfaceStyle 2>/dev/null | grep -qx Dark && echo dark || echo light)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Measuring a theme
|
|
159
|
+
|
|
160
|
+
`vim2bat compare THEME FILE...` reports how often the theme gives each
|
|
161
|
+
character the color Vim does, per file and per language, then the most common
|
|
162
|
+
mismatches:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
vim2bat compare vim-retrobox-dark app/models/*.rb
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Expect 85 to 90 percent on real code in the languages Table 3 covers (see
|
|
169
|
+
below): bat's syntax definitions don't split code exactly the way Vim's do.
|
|
170
|
+
Other languages get only Table 2 and score lower.
|
|
171
|
+
|
|
172
|
+
## How it works
|
|
173
|
+
|
|
174
|
+
1. For each background, vim2bat runs Vim with your vimrc on a pseudo-terminal,
|
|
175
|
+
loads the scheme, and records each highlight group's colors and attributes
|
|
176
|
+
(Table 1, cached in `$XDG_CACHE_HOME/vim2bat/tables`, or
|
|
177
|
+
`~/.cache/vim2bat/tables`). Vim gets a terminal because
|
|
178
|
+
without one it numbers named colors such as `DarkBlue` differently.
|
|
179
|
+
2. `src/vim2bat/share/strict.tsv` (Table 2) maps Vim's standard groups, such
|
|
180
|
+
as `Comment` and `String`, to the TextMate scopes with the same meaning.
|
|
181
|
+
3. `src/vim2bat/share/loose.tsv` (Table 3) maps language-specific scopes to the
|
|
182
|
+
Vim syntax groups that Vim gives the same tokens, for Ruby, ERB, HTML, CSS
|
|
183
|
+
and SCSS, TypeScript and TSX, JavaScript and JSX, YAML, Vim script, git
|
|
184
|
+
commit messages and diffs. The languages and tokens follow what
|
|
185
|
+
vim-solarized8 colors.
|
|
186
|
+
4. Palette colors use bat's palette notation, where `#0E000000` means slot 14,
|
|
187
|
+
and the terminal's default color is `#00000001`.
|
|
188
|
+
|
|
189
|
+
## Known limits
|
|
190
|
+
|
|
191
|
+
- bat doesn't paint a scheme's background color, so a scheme with its own
|
|
192
|
+
background, such as habamax, sits on your terminal's.
|
|
193
|
+
- bat themes support only bold, italic and underline, so reverse and undercurl
|
|
194
|
+
are dropped.
|
|
195
|
+
- `-c` deletes the whole bat themes folder, including themes vim2bat didn't
|
|
196
|
+
build.
|
|
197
|
+
- A single-background scheme whose own name ends in `-dark` or `-light` gets a
|
|
198
|
+
name that looks like another scheme's background variant.
|
|
199
|
+
- Colorschemes in a subfolder of `colors/`, whose names contain `/`, aren't
|
|
200
|
+
supported. vim2bat rejects those names.
|
|
201
|
+
|
|
202
|
+
## Uninstall
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
brew uninstall vim2bat # or: uv tool uninstall vim2bat, or pipx uninstall vim2bat
|
|
206
|
+
rm "$(bat --config-dir)"/themes/vim-*.tmTheme && (cd / && bat cache --build)
|
|
207
|
+
rm -rf "${XDG_CACHE_HOME:-$HOME/.cache}/vim2bat"
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Development
|
|
211
|
+
|
|
212
|
+
Development uses [uv](https://docs.astral.sh/uv/) (`brew install uv`).
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
git clone https://github.com/redodson01/vim2bat.git
|
|
216
|
+
cd vim2bat
|
|
217
|
+
uv sync # creates .venv with vim2bat and the dev tools
|
|
218
|
+
uv run pre-commit install # runs ruff and file checks before each commit
|
|
219
|
+
uv run pytest # unit tests, plus integration tests when vim and bat are installed
|
|
220
|
+
uv run mypy # type checks src and tests
|
|
221
|
+
uv run ruff check # lints
|
|
222
|
+
uv run ruff format # formats
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
[CONTRIBUTING.md](https://github.com/redodson01/vim2bat/blob/main/CONTRIBUTING.md)
|
|
226
|
+
covers the golden files, measuring a change, the rules for the tables, and
|
|
227
|
+
releasing.
|
|
228
|
+
|
|
229
|
+
## License
|
|
230
|
+
|
|
231
|
+
[MIT](https://github.com/redodson01/vim2bat/blob/main/LICENSE)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
vim2bat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
vim2bat/__main__.py,sha256=1UwKOIuKO4tg-OJmllVm1m-JuWMPSkFBdMEm1f1HJkU,59
|
|
3
|
+
vim2bat/cli.py,sha256=n_j9PKHO7tUtbIdg-ln23JAFjgwjpRXDPBnQ1DJjBWA,27725
|
|
4
|
+
vim2bat/share/loose.tsv,sha256=F62TtLnNvMB4yiXMz1xpmFaLuXn6NHSm4pvaO_aHb00,7945
|
|
5
|
+
vim2bat/share/strict.tsv,sha256=ik4d5ZmLy68EHirQvB8I96WkBNOWzD1YZ6AChBpsH6U,1829
|
|
6
|
+
vim2bat/share/vim-groups.vim,sha256=BtplNbxbAvDYri4P4KCRMSGkqN7m4WGvsnSycWm2F7c,2381
|
|
7
|
+
vim2bat/share/vim-slots.vim,sha256=SXuOtiHud2vaEX-BDA8XAHR7C0sYCrqG4KVmLmZebp8,2661
|
|
8
|
+
vim2bat-0.1.0.dist-info/METADATA,sha256=gxE4NgwgTfFf4TBauOC0lKXy1AjWrymBXCh-iTepckk,8667
|
|
9
|
+
vim2bat-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
10
|
+
vim2bat-0.1.0.dist-info/entry_points.txt,sha256=L0E9B0oCMWjAb1bZqt-pzqknvwM_N3BpHwC_eYdabVc,45
|
|
11
|
+
vim2bat-0.1.0.dist-info/licenses/LICENSE,sha256=sCNhPkNLzVVbaoxdKKVGk_QAbxxx4YDrNfX_7vQGFis,1074
|
|
12
|
+
vim2bat-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Richard E. Dodson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|