dktcli 1.0.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.
- devkittools/__init__.py +3 -0
- devkittools/__main__.py +6 -0
- devkittools/cli.py +179 -0
- devkittools/tools/__init__.py +1 -0
- devkittools/tools/bench.py +73 -0
- devkittools/tools/changeloggen.py +181 -0
- devkittools/tools/clean.py +110 -0
- devkittools/tools/colortool.py +126 -0
- devkittools/tools/commitwriter.py +199 -0
- devkittools/tools/contextpacker.py +226 -0
- devkittools/tools/cron.py +152 -0
- devkittools/tools/doctor.py +74 -0
- devkittools/tools/du.py +72 -0
- devkittools/tools/enc.py +131 -0
- devkittools/tools/envtool.py +141 -0
- devkittools/tools/findtool.py +119 -0
- devkittools/tools/gen.py +182 -0
- devkittools/tools/gx.py +184 -0
- devkittools/tools/hashtool.py +93 -0
- devkittools/tools/httptool.py +131 -0
- devkittools/tools/ignore.py +213 -0
- devkittools/tools/initproj.py +166 -0
- devkittools/tools/jsontool.py +200 -0
- devkittools/tools/net.py +114 -0
- devkittools/tools/portkiller.py +246 -0
- devkittools/tools/regextool.py +84 -0
- devkittools/tools/repostats.py +197 -0
- devkittools/tools/secrets_scan.py +108 -0
- devkittools/tools/serve.py +118 -0
- devkittools/tools/snip.py +94 -0
- devkittools/tools/system.py +157 -0
- devkittools/tools/timetool.py +130 -0
- devkittools/tools/todo.py +69 -0
- devkittools/tools/tree.py +86 -0
- devkittools/tools/watch.py +82 -0
- devkittools/utils.py +561 -0
- dktcli-1.0.0.dist-info/METADATA +148 -0
- dktcli-1.0.0.dist-info/RECORD +42 -0
- dktcli-1.0.0.dist-info/WHEEL +5 -0
- dktcli-1.0.0.dist-info/entry_points.txt +3 -0
- dktcli-1.0.0.dist-info/licenses/LICENSE +21 -0
- dktcli-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""dkt time (tm) - convert timestamps, show time in many formats, compute differences."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import re
|
|
6
|
+
import time
|
|
7
|
+
from datetime import datetime, timedelta, timezone
|
|
8
|
+
|
|
9
|
+
from ..utils import DktError, c, kv, output_result
|
|
10
|
+
|
|
11
|
+
META = ("Generate & Convert", "time", "tm", "epoch <-> date converter, 'time ago', date math")
|
|
12
|
+
|
|
13
|
+
EPILOG = """\
|
|
14
|
+
examples:
|
|
15
|
+
dkt tm now in every common format
|
|
16
|
+
dkt tm 1700000000 epoch -> date (seconds, ms or us auto-detected)
|
|
17
|
+
dkt tm 2024-03-01T12:00:00Z date -> epoch
|
|
18
|
+
dkt tm "2024-03-01 09:30" --tz +05:30
|
|
19
|
+
dkt tm now --add 3d4h date math (units: s m h d w)
|
|
20
|
+
dkt tm 2024-01-01 --diff 2024-03-15 difference between two moments
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def parse_offset(s: str) -> timezone:
|
|
25
|
+
if s.upper() in ("Z", "UTC"):
|
|
26
|
+
return timezone.utc
|
|
27
|
+
m = re.fullmatch(r"([+-])(\d{1,2})(?::?(\d{2}))?", s)
|
|
28
|
+
if not m:
|
|
29
|
+
raise DktError("Bad timezone '%s'. Use UTC or an offset like +05:30 / -08." % s)
|
|
30
|
+
delta = timedelta(hours=int(m.group(2)), minutes=int(m.group(3) or 0))
|
|
31
|
+
return timezone(delta if m.group(1) == "+" else -delta)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def parse_moment(s: str, assume_tz: timezone) -> datetime:
|
|
35
|
+
s = s.strip()
|
|
36
|
+
if s.lower() in ("", "now"):
|
|
37
|
+
return datetime.now(timezone.utc)
|
|
38
|
+
if re.fullmatch(r"-?\d+(\.\d+)?", s):
|
|
39
|
+
v = float(s)
|
|
40
|
+
a = abs(v)
|
|
41
|
+
if a >= 1e17:
|
|
42
|
+
v /= 1e9
|
|
43
|
+
elif a >= 1e14:
|
|
44
|
+
v /= 1e6
|
|
45
|
+
elif a >= 1e11:
|
|
46
|
+
v /= 1e3
|
|
47
|
+
return datetime.fromtimestamp(v, tz=timezone.utc)
|
|
48
|
+
t = s.replace("Z", "+00:00").replace("z", "+00:00")
|
|
49
|
+
for candidate in (t, t.replace(" ", "T", 1)):
|
|
50
|
+
try:
|
|
51
|
+
dt = datetime.fromisoformat(candidate)
|
|
52
|
+
return dt if dt.tzinfo else dt.replace(tzinfo=assume_tz)
|
|
53
|
+
except ValueError:
|
|
54
|
+
pass
|
|
55
|
+
for fmt in ("%Y/%m/%d %H:%M:%S", "%Y/%m/%d", "%d/%m/%Y", "%m/%d/%Y %H:%M", "%b %d %Y", "%d %b %Y", "%B %d, %Y"):
|
|
56
|
+
try:
|
|
57
|
+
return datetime.strptime(s, fmt).replace(tzinfo=assume_tz)
|
|
58
|
+
except ValueError:
|
|
59
|
+
pass
|
|
60
|
+
raise DktError("Couldn't understand '%s'. Try epoch seconds or ISO like 2024-03-01T12:00:00Z." % s)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def parse_delta(s: str) -> timedelta:
|
|
64
|
+
total = 0
|
|
65
|
+
found = False
|
|
66
|
+
for num, unit in re.findall(r"(-?\d+)\s*([smhdw])", s.lower()):
|
|
67
|
+
found = True
|
|
68
|
+
total += int(num) * {"s": 1, "m": 60, "h": 3600, "d": 86400, "w": 604800}[unit]
|
|
69
|
+
if not found:
|
|
70
|
+
raise DktError("Bad duration '%s'. Example: 3d4h30m" % s)
|
|
71
|
+
return timedelta(seconds=total)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def human_delta(td: timedelta) -> str:
|
|
75
|
+
secs = int(abs(td.total_seconds()))
|
|
76
|
+
parts = []
|
|
77
|
+
for name, n in (("y", 31536000), ("d", 86400), ("h", 3600), ("m", 60), ("s", 1)):
|
|
78
|
+
if secs >= n:
|
|
79
|
+
parts.append("%d%s" % (secs // n, name))
|
|
80
|
+
secs %= n
|
|
81
|
+
return " ".join(parts[:3]) or "0s"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def relative(dt: datetime) -> str:
|
|
85
|
+
d = dt - datetime.now(timezone.utc)
|
|
86
|
+
return ("in " + human_delta(d)) if d.total_seconds() > 0 else (human_delta(d) + " ago")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def run(args) -> int:
|
|
90
|
+
tz = parse_offset(args.tz) if args.tz else datetime.now().astimezone().tzinfo
|
|
91
|
+
dt = parse_moment(args.moment or "now", tz)
|
|
92
|
+
if args.add:
|
|
93
|
+
dt += parse_delta(args.add)
|
|
94
|
+
if args.diff:
|
|
95
|
+
other = parse_moment(args.diff, tz)
|
|
96
|
+
d = other - dt
|
|
97
|
+
kv([("from", dt.isoformat()), ("to", other.isoformat()),
|
|
98
|
+
("difference", c(human_delta(d), "bold") + (" later" if d.total_seconds() >= 0 else " earlier")),
|
|
99
|
+
("seconds", "%d" % d.total_seconds()), ("days", "%.2f" % (d.total_seconds() / 86400))])
|
|
100
|
+
return 0
|
|
101
|
+
local = dt.astimezone(tz) if args.tz else dt.astimezone()
|
|
102
|
+
epoch = dt.timestamp()
|
|
103
|
+
rows = [("UTC", dt.astimezone(timezone.utc).strftime("%Y-%m-%d %H:%M:%S"))]
|
|
104
|
+
if not (args.tz and args.tz.upper() in ("UTC", "Z")):
|
|
105
|
+
rows.append((args.tz.upper() if args.tz else "local", local.strftime("%Y-%m-%d %H:%M:%S %z")))
|
|
106
|
+
rows += [
|
|
107
|
+
("ISO 8601", dt.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")),
|
|
108
|
+
("epoch (s)", "%d" % epoch),
|
|
109
|
+
("epoch (ms)", "%d" % (epoch * 1000)),
|
|
110
|
+
("RFC 2822", local.strftime("%a, %d %b %Y %H:%M:%S %z")),
|
|
111
|
+
("day", "%s, week %s, day %s of year" % (local.strftime("%A"), local.strftime("%V"), local.strftime("%j"))),
|
|
112
|
+
("relative", relative(dt)),
|
|
113
|
+
]
|
|
114
|
+
if args.raw:
|
|
115
|
+
print("%d" % epoch)
|
|
116
|
+
return 0
|
|
117
|
+
kv([(k, c(v, "bold") if k in ("epoch (s)", "ISO 8601") else v) for k, v in rows])
|
|
118
|
+
return 0
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def register(sub, common) -> None:
|
|
122
|
+
p = sub.add_parser("time", aliases=["tm"], parents=[common], help=META[3],
|
|
123
|
+
description="Convert and inspect timestamps.", epilog=EPILOG,
|
|
124
|
+
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
125
|
+
p.add_argument("moment", nargs="?", help="epoch, ISO date, or 'now' (default)")
|
|
126
|
+
p.add_argument("--tz", metavar="OFFSET", help="timezone offset for input/output, e.g. +05:30 or UTC")
|
|
127
|
+
p.add_argument("--add", metavar="DUR", help="add a duration, e.g. 3d4h or -90m")
|
|
128
|
+
p.add_argument("--diff", metavar="OTHER", help="show the difference to another moment")
|
|
129
|
+
p.add_argument("--raw", action="store_true", help="print only epoch seconds")
|
|
130
|
+
p.set_defaults(func=run)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""dkt todo (td) - collect TODO / FIXME / HACK comments across a project."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from collections import Counter
|
|
8
|
+
|
|
9
|
+
from ..utils import c, is_binary_chunk, list_project_files, resolve_folder, sym, warn, human_num
|
|
10
|
+
|
|
11
|
+
META = ("Project & Code", "todo", "td", "list TODO/FIXME/HACK comments with file:line")
|
|
12
|
+
|
|
13
|
+
EPILOG = """\
|
|
14
|
+
examples:
|
|
15
|
+
dkt td scan the current project
|
|
16
|
+
dkt td --tags FIXME BUG only these tags
|
|
17
|
+
dkt td --json machine-readable
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
DEFAULT_TAGS = ["TODO", "FIXME", "HACK", "XXX", "BUG", "OPTIMIZE", "NOTE"]
|
|
21
|
+
TAG_COLOR = {"TODO": "bcyan", "FIXME": "bred", "BUG": "bred", "HACK": "byellow", "XXX": "byellow",
|
|
22
|
+
"OPTIMIZE": "bmagenta", "NOTE": "bgreen"}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def run(args) -> int:
|
|
26
|
+
folder = resolve_folder(args.folder)
|
|
27
|
+
tags = [t.upper() for t in (args.tags or DEFAULT_TAGS)]
|
|
28
|
+
rx = re.compile(r"(?:#|//|/\*|<!--|--|;|\*|%)\s*(?:@)?\b(" + "|".join(map(re.escape, tags)) + r")\b[\s:(\-]*(.*)", re.I)
|
|
29
|
+
found = []
|
|
30
|
+
for rel in list_project_files(folder):
|
|
31
|
+
p = folder / rel
|
|
32
|
+
try:
|
|
33
|
+
if p.stat().st_size > 1_000_000:
|
|
34
|
+
continue
|
|
35
|
+
raw = p.read_bytes()
|
|
36
|
+
except OSError:
|
|
37
|
+
continue
|
|
38
|
+
if is_binary_chunk(raw[:4096]):
|
|
39
|
+
continue
|
|
40
|
+
for i, line in enumerate(raw.decode("utf-8", errors="replace").splitlines(), 1):
|
|
41
|
+
m = rx.search(line)
|
|
42
|
+
if m:
|
|
43
|
+
found.append({"file": str(rel).replace("\\", "/"), "line": i,
|
|
44
|
+
"tag": m.group(1).upper(), "text": m.group(2).strip(" */-->")[:120]})
|
|
45
|
+
if args.json:
|
|
46
|
+
print(json.dumps(found, indent=2))
|
|
47
|
+
return 0
|
|
48
|
+
if not found:
|
|
49
|
+
print(" %s nothing to do - no %s found" % (c(sym("ok"), "bgreen"), "/".join(tags[:3])))
|
|
50
|
+
return 0
|
|
51
|
+
cur = None
|
|
52
|
+
for f in found:
|
|
53
|
+
if f["file"] != cur:
|
|
54
|
+
cur = f["file"]
|
|
55
|
+
print("\n " + c(cur, "bold underline"))
|
|
56
|
+
print(" %s %s %s" % (c(str(f["line"]).rjust(5), "gray"), c(f["tag"].ljust(8), TAG_COLOR.get(f["tag"], "white")), f["text"]))
|
|
57
|
+
counts = Counter(f["tag"] for f in found)
|
|
58
|
+
print("\n " + " ".join("%s %s" % (c(human_num(n), "bold"), t) for t, n in counts.most_common()))
|
|
59
|
+
return 0
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def register(sub, common) -> None:
|
|
63
|
+
p = sub.add_parser("todo", aliases=["td"], parents=[common], help=META[3],
|
|
64
|
+
description="Find TODO-style comments in your project (respects .gitignore).",
|
|
65
|
+
epilog=EPILOG, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
66
|
+
p.add_argument("-f", "--folder", metavar="PATH")
|
|
67
|
+
p.add_argument("--tags", nargs="+", metavar="TAG")
|
|
68
|
+
p.add_argument("--json", action="store_true")
|
|
69
|
+
p.set_defaults(func=run)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""dkt tree (tr) - a pretty directory tree that respects .gitignore."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from ..utils import c, human_size, list_project_files, resolve_folder, sym
|
|
9
|
+
|
|
10
|
+
META = ("Project & Code", "tree", "tr", "pretty directory tree with sizes, respects .gitignore")
|
|
11
|
+
|
|
12
|
+
EPILOG = """\
|
|
13
|
+
examples:
|
|
14
|
+
dkt tr tree of the current project
|
|
15
|
+
dkt tr -d 2 only two levels deep
|
|
16
|
+
dkt tr --sizes show file sizes
|
|
17
|
+
dkt tr --dirs directories only
|
|
18
|
+
dkt tr src --copy copy the tree to the clipboard (great for docs / AI prompts)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
ICONS = {".py": "bgreen", ".js": "byellow", ".ts": "bblue", ".json": "yellow", ".md": "bcyan",
|
|
22
|
+
".html": "bred", ".css": "bmagenta", ".bat": "gray", ".toml": "yellow", ".yml": "yellow", ".yaml": "yellow"}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def run(args) -> int:
|
|
26
|
+
root = resolve_folder(args.folder)
|
|
27
|
+
files = list_project_files(root)
|
|
28
|
+
tree = defaultdict(lambda: {"dirs": set(), "files": []})
|
|
29
|
+
for f in files:
|
|
30
|
+
parts = f.parts
|
|
31
|
+
for i in range(len(parts) - 1):
|
|
32
|
+
tree[Path(*parts[:i]) if i else Path(".")]["dirs"].add(parts[i])
|
|
33
|
+
tree[f.parent if len(parts) > 1 else Path(".")]["files"].append(f.name)
|
|
34
|
+
plain = []
|
|
35
|
+
out = []
|
|
36
|
+
counts = [0, 0]
|
|
37
|
+
T, L, P = sym("t") + sym("h") * 2 + " ", sym("l") + sym("h") * 2 + " ", sym("pipe") + " "
|
|
38
|
+
|
|
39
|
+
def walk(rel: Path, prefix: str, depth: int):
|
|
40
|
+
node = tree.get(rel, {"dirs": set(), "files": []})
|
|
41
|
+
entries = [(d, True) for d in sorted(node["dirs"], key=str.lower)]
|
|
42
|
+
if not args.dirs:
|
|
43
|
+
entries += [(f, False) for f in sorted(node["files"], key=str.lower)]
|
|
44
|
+
for i, (name, is_dir) in enumerate(entries):
|
|
45
|
+
last = i == len(entries) - 1
|
|
46
|
+
branch = L if last else T
|
|
47
|
+
if is_dir:
|
|
48
|
+
counts[0] += 1
|
|
49
|
+
out.append(prefix + c(branch, "gray") + c(name + "/", "bold bblue"))
|
|
50
|
+
plain.append(prefix + branch + name + "/")
|
|
51
|
+
if args.depth is None or depth < args.depth:
|
|
52
|
+
walk(rel / name if str(rel) != "." else Path(name), prefix + (" " if last else P), depth + 1)
|
|
53
|
+
else:
|
|
54
|
+
counts[1] += 1
|
|
55
|
+
size = ""
|
|
56
|
+
if args.sizes:
|
|
57
|
+
try:
|
|
58
|
+
size = " " + human_size((root / (rel / name)).stat().st_size)
|
|
59
|
+
except OSError:
|
|
60
|
+
size = ""
|
|
61
|
+
col = ICONS.get(Path(name).suffix.lower(), "white")
|
|
62
|
+
out.append(prefix + c(branch, "gray") + c(name, col) + c(size, "gray"))
|
|
63
|
+
plain.append(prefix + branch + name + size)
|
|
64
|
+
|
|
65
|
+
out.append(c(root.name + "/", "bold bmagenta"))
|
|
66
|
+
plain.append(root.name + "/")
|
|
67
|
+
walk(Path("."), "", 1)
|
|
68
|
+
print("\n".join(out))
|
|
69
|
+
print(c("\n %d directories, %d files" % tuple(counts), "gray"))
|
|
70
|
+
if args.copy:
|
|
71
|
+
from ..utils import copy_to_clipboard, info
|
|
72
|
+
if copy_to_clipboard("\n".join(plain)):
|
|
73
|
+
info(c(sym("ok") + " copied to clipboard", "green"))
|
|
74
|
+
return 0
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def register(sub, common) -> None:
|
|
78
|
+
p = sub.add_parser("tree", aliases=["tr"], parents=[common], help=META[3],
|
|
79
|
+
description="Show a directory tree (git-ignored files and junk folders are skipped).",
|
|
80
|
+
epilog=EPILOG, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
81
|
+
p.add_argument("folder", nargs="?")
|
|
82
|
+
p.add_argument("-d", "--depth", type=int, metavar="N")
|
|
83
|
+
p.add_argument("--sizes", action="store_true")
|
|
84
|
+
p.add_argument("--dirs", action="store_true", help="directories only")
|
|
85
|
+
p.add_argument("--copy", action="store_true")
|
|
86
|
+
p.set_defaults(func=run)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""dkt watch (w) - re-run a command whenever files change."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
from ..utils import DktError, c, list_project_files, resolve_folder, sym
|
|
10
|
+
|
|
11
|
+
META = ("Project & Code", "watch", "w", "re-run any command when files change")
|
|
12
|
+
|
|
13
|
+
EPILOG = """\
|
|
14
|
+
examples:
|
|
15
|
+
dkt w -c "pytest -q" run tests on every save
|
|
16
|
+
dkt w -c "npm run build" --ext .js .ts
|
|
17
|
+
dkt w -c "python main.py" --clear --debounce 0.5
|
|
18
|
+
dkt w -c "make" -f src --interval 0.3
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def snapshot(root, exts):
|
|
23
|
+
snap = {}
|
|
24
|
+
for rel in list_project_files(root):
|
|
25
|
+
if exts and not str(rel).lower().endswith(exts):
|
|
26
|
+
continue
|
|
27
|
+
try:
|
|
28
|
+
st = (root / rel).stat()
|
|
29
|
+
snap[str(rel)] = (st.st_mtime_ns, st.st_size)
|
|
30
|
+
except OSError:
|
|
31
|
+
pass
|
|
32
|
+
return snap
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def run(args) -> int:
|
|
36
|
+
root = resolve_folder(args.folder)
|
|
37
|
+
exts = tuple(e if e.startswith(".") else "." + e for e in args.ext) if args.ext else None
|
|
38
|
+
if not args.command:
|
|
39
|
+
raise DktError('Tell me what to run, e.g. dkt w -c "pytest -q"')
|
|
40
|
+
prev = snapshot(root, exts)
|
|
41
|
+
runs = 0
|
|
42
|
+
|
|
43
|
+
def execute(reason: str):
|
|
44
|
+
nonlocal runs
|
|
45
|
+
runs += 1
|
|
46
|
+
if args.clear:
|
|
47
|
+
os.system("cls" if os.name == "nt" else "clear")
|
|
48
|
+
print("%s %s %s" % (c(sym("arrow"), "bcyan"), c(time.strftime("%H:%M:%S"), "gray"), c(args.command, "bold")))
|
|
49
|
+
if reason:
|
|
50
|
+
print(c(" changed: " + reason, "gray"))
|
|
51
|
+
t0 = time.time()
|
|
52
|
+
rc = subprocess.call(args.command, shell=True, cwd=str(root))
|
|
53
|
+
dt = time.time() - t0
|
|
54
|
+
col, mark = ("bgreen", sym("ok")) if rc == 0 else ("bred", sym("fail"))
|
|
55
|
+
print("%s %s\n" % (c(mark + " exit %d" % rc, col), c("(%.2fs) waiting for changes... Ctrl+C to stop" % dt, "gray")))
|
|
56
|
+
|
|
57
|
+
execute("")
|
|
58
|
+
try:
|
|
59
|
+
while True:
|
|
60
|
+
time.sleep(args.interval)
|
|
61
|
+
cur = snapshot(root, exts)
|
|
62
|
+
if cur != prev:
|
|
63
|
+
changed = [k for k in set(cur) | set(prev) if cur.get(k) != prev.get(k)]
|
|
64
|
+
time.sleep(args.debounce)
|
|
65
|
+
prev = snapshot(root, exts)
|
|
66
|
+
execute(", ".join(sorted(changed)[:3]) + (" ..." if len(changed) > 3 else ""))
|
|
67
|
+
except KeyboardInterrupt:
|
|
68
|
+
print("\n stopped after %d run%s." % (runs, "" if runs == 1 else "s"))
|
|
69
|
+
return 0
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def register(sub, common) -> None:
|
|
73
|
+
p = sub.add_parser("watch", aliases=["w"], parents=[common], help=META[3],
|
|
74
|
+
description="Watch a folder and re-run a command on changes (no dependencies).",
|
|
75
|
+
epilog=EPILOG, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
76
|
+
p.add_argument("-c", "--command", metavar="CMD", help="command to run")
|
|
77
|
+
p.add_argument("-f", "--folder", metavar="PATH", help="folder to watch (default: current)")
|
|
78
|
+
p.add_argument("--ext", nargs="+", metavar="EXT", help="only react to these extensions")
|
|
79
|
+
p.add_argument("--interval", type=float, default=0.7, help="poll seconds (default 0.7)")
|
|
80
|
+
p.add_argument("--debounce", type=float, default=0.3, help="settle time after a change")
|
|
81
|
+
p.add_argument("--clear", action="store_true", help="clear the screen before each run")
|
|
82
|
+
p.set_defaults(func=run)
|