~kris/dots

srice

ref: 178c9a25a484ead715392d50e21d03df486b8b41 srice/.config/.mksh/author.sh -rw-r--r-- 25.4 KiB
178c9a25 — Kris Yotam conky: sync proper config from moirai (was the greenred variant) 2 months ago
                                                                                
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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
# author -- writing tools: dictionaries, prose analysis, translation, publishing

# ============================================================================
# DICTIONARY / THESAURUS
# ============================================================================
alias d='dict'
alias dg='dict -d gcide'
alias dw='dict -d wn'
alias dmt='dict -d moby-thesaurus'
alias dd='dict -d devil'

# ============================================================================
# TRANSLATION
# ============================================================================
alias t='trans -b'
alias ten='trans -b :en'
alias tdict='trans -d en:'

# ============================================================================
# PROSE LINTING
# ============================================================================
alias vl='vale'
alias pl='proselint'
alias wg='write-good'
alias ax='alex'
alias lt='languagetool-commandline'
alias tl='textlint'
alias hp='harper'
alias dic='diction'
alias sty='style'
alias wdf='wdiff'
alias sq='smartypants'
alias ltj='java -jar /usr/share/java/languagetool/languagetool-commandline.jar -l en-US'

# ============================================================================
# PANDOC SHORTCUTS
# ============================================================================
alias md2pdf='pandoc --pdf-engine=xelatex'
alias md2docx='pandoc --reference-doc=reference.docx'
alias md2epub='pandoc -t epub3 --toc'
alias md2html='pandoc --standalone --toc'
alias md2rtf='pandoc -s -t rtf'

# ============================================================================
# CALIBRE / EBOOK
# ============================================================================
alias ec='ebook-convert'
alias em='ebook-meta'
alias ep='ebook-polish'
alias epv='epubcheck'

# ============================================================================
# ENCODING / CLEANUP
# ============================================================================
alias cdet='chardetect'
alias dtx='detox -n'
alias dtxf='detox'

# ============================================================================
# BIBLIOGRAPHY
# ============================================================================
alias pap='papis'
alias d2b='doi2bib'

# ============================================================================
# CRITICMARKUP
# ============================================================================
alias pc='pancritic'

# ============================================================================
# FUNCTIONS
# ============================================================================

# --- DICTIONARIES ---

# etym -- etymology lookup via etymonline
etym() {
    curl -s "https://www.etymonline.com/word/$1" | python3 -c "
import sys
from html.parser import HTMLParser
class P(HTMLParser):
    def __init__(self):
        super().__init__()
        self.capture = False
        self.text = []
    def handle_starttag(self, tag, attrs):
        for k, v in attrs:
            if k == 'class' and 'word__defination' in (v or ''):
                self.capture = True
    def handle_endtag(self, tag):
        if tag == 'section': self.capture = False
    def handle_data(self, data):
        if self.capture: self.text.append(data)
p = P()
p.feed(sys.stdin.read())
print(' '.join(p.text).strip()[:2000])
" 2>/dev/null
}

# syn -- synonyms via Datamuse API
syn() {
    curl -s "https://api.datamuse.com/words?rel_syn=$1&max=30" | python3 -c "import json,sys; [print(w['word']) for w in json.load(sys.stdin)]"
}

# ant -- antonyms via Datamuse
ant() {
    curl -s "https://api.datamuse.com/words?rel_ant=$1&max=20" | python3 -c "import json,sys; [print(w['word']) for w in json.load(sys.stdin)]"
}

# rhyme -- find rhymes
rhyme() {
    echo "Perfect:"
    curl -s "https://api.datamuse.com/words?rel_rhy=$1&max=25" | python3 -c "import json,sys; [print(f'  {w[\"word\"]}') for w in json.load(sys.stdin)]"
    echo "Slant:"
    curl -s "https://api.datamuse.com/words?rel_nry=$1&max=15" | python3 -c "import json,sys; [print(f'  {w[\"word\"]}') for w in json.load(sys.stdin)]"
}

# meanslike -- semantic similarity search
meanslike() {
    typeset query
    query=$(echo "$*" | tr ' ' '+')
    curl -s "https://api.datamuse.com/words?ml=$query&max=20" | python3 -c "import json,sys; [print(w['word']) for w in json.load(sys.stdin)]"
}

# soundslike -- phonetic similarity
soundslike() {
    curl -s "https://api.datamuse.com/words?sl=$1&max=20" | python3 -c "import json,sys; [print(w['word']) for w in json.load(sys.stdin)]"
}

# follows -- words that often follow X
follows() {
    curl -s "https://api.datamuse.com/words?lc=$1&max=20" | python3 -c "import json,sys; [print(w['word']) for w in json.load(sys.stdin)]"
}

# precedes -- words that often precede X
precedes() {
    curl -s "https://api.datamuse.com/words?rc=$1&max=20" | python3 -c "import json,sys; [print(w['word']) for w in json.load(sys.stdin)]"
}

# adjfor -- adjectives used to describe a noun
adjfor() {
    curl -s "https://api.datamuse.com/words?rel_jjb=$1&max=25" | python3 -c "import json,sys; [print(w['word']) for w in json.load(sys.stdin)]"
}

# nounsfor -- nouns described by an adjective
nounsfor() {
    curl -s "https://api.datamuse.com/words?rel_jja=$1&max=25" | python3 -c "import json,sys; [print(w['word']) for w in json.load(sys.stdin)]"
}

# homophones -- words that sound the same
homophones() {
    curl -s "https://api.datamuse.com/words?rel_hom=$1" | python3 -c "import json,sys; [print(w['word']) for w in json.load(sys.stdin)]"
}

# --- TRANSLATION ---

# tpoly -- translate to multiple languages
tpoly() {
    typeset word="$1"; shift
    typeset lang
    for lang in "$@"; do
        printf "%-4s: %s\n" "$lang" "$(trans -b en:$lang "$word" 2>/dev/null)"
    done
}

# cognates -- compare a word across language families
cognates() {
    typeset word="$1"
    typeset family="$2"
    typeset langs
    case "$family" in
        romance)  langs="fr es it pt ro la" ;;
        germanic) langs="de nl sv da no" ;;
        slavic)   langs="ru pl cs hr bg uk" ;;
        *)        langs="la fr de ru grc sa" ;;
    esac
    printf "%-6s %-30s\n" "LANG" "TRANSLATION"
    typeset lang
    for lang in $langs; do
        printf "%-6s %s\n" "$lang" "$(trans -b en:$lang "$word" 2>/dev/null)"
    done
}

# ipa -- IPA transcription via espeak-ng
ipa() {
    espeak-ng -q --ipa "$*"
}

# --- PROSE ANALYSIS ---

# wc-git -- word count diff between commits
wc-git() {
    typeset prev="${1:-HEAD~1}"
    typeset curr="${2:-HEAD}"
    typeset added removed net
    added=$(git diff "$prev" "$curr" --word-diff=porcelain | grep '^+[^+]' | sed 's/^+//' | wc -w)
    removed=$(git diff "$prev" "$curr" --word-diff=porcelain | grep '^-[^-]' | sed 's/^-//' | wc -w)
    net=$((added - removed))
    echo "Added:   +$added words"
    echo "Removed: -$removed words"
    echo "Net:     $net words"
}

# readability -- readability scores for a file
readability() {
    python3 -c "
import textstat, sys
text = open('$1').read()
print(f'Flesch Reading Ease:  {textstat.flesch_reading_ease(text):.1f}')
print(f'Flesch-Kincaid Grade: {textstat.flesch_kincaid_grade(text):.1f}')
print(f'Gunning FOG:          {textstat.gunning_fog(text):.1f}')
print(f'Consensus:            {textstat.text_standard(text)}')
print(f'Words:                {textstat.lexicon_count(text)}')
print(f'Sentences:            {textstat.sentence_count(text)}')
"
}

# wordfreq -- word frequency on the Zipf scale
wordfreq() {
    python3 -c "
from wordfreq import zipf_frequency
print(f'{zipf_frequency(\"$1\", \"en\"):.2f}  (1=rare, 3=uncommon, 5=common, 7=extremely common)')
"
}

# ttr -- type-token ratio for a file
ttr() {
    python3 -c "
import sys, re
from collections import Counter
text = open('$1').read().lower()
words = re.findall(r'[a-z]+', text)
types = len(set(words))
tokens = len(words)
print(f'Types: {types}  Tokens: {tokens}  TTR: {types/tokens:.4f}')
"
}

# --- PUBLISHING ---

# compile -- assemble chapters into a manuscript
compile() {
    typeset fmt="${1:-pdf}"
    typeset chapters
    chapters=$(ls chapters/*.md 2>/dev/null | sort -V)
    if [[ -z $chapters ]]; then
        echo "No chapters/*.md found"
        return 1
    fi
    mkdir -p build
    case "$fmt" in
        pdf)  pandoc $chapters --pdf-engine=xelatex -V geometry:margin=1in -V fontsize:12pt -V linestretch:2 --toc -o build/manuscript.pdf ;;
        docx) pandoc $chapters --reference-doc=reference.docx -o build/manuscript.docx ;;
        epub) pandoc $chapters -t epub3 --toc -o build/manuscript.epub ;;
        html) pandoc $chapters --standalone -o build/manuscript.html ;;
    esac
    echo "Built: build/manuscript.$fmt"
}

# isbn -- ISBN metadata lookup
isbn() {
    python3 -c "
from isbntools.app import meta
info = meta('$1')
for k, v in info.items():
    print(f'{k}: {v}')
" 2>/dev/null || echo "pip install isbntools"
}

# --- STYLE ANALYSIS ---

# cliches -- detect cliched phrases in a file
cliches() {
    typeset file="${1:?Usage: cliches <file>}"
    python3 -c "
import re, sys
CLICHES = [
    'at the end of the day', 'it goes without saying', 'in the final analysis',
    'all things considered', 'when all is said and done', 'at this point in time',
    'for all intents and purposes', 'few and far between', 'in the nick of time',
    'last but not least', 'needless to say', 'the fact of the matter',
    'par for the course', 'a level playing field', 'think outside the box',
    'low-hanging fruit', 'move the needle', 'at the crack of dawn',
    'dead as a doornail', 'fit as a fiddle', 'good as gold', 'old as the hills',
    'sick and tired', 'tried and true', 'calm before the storm',
    'tip of the iceberg', 'light at the end of the tunnel', 'in a nutshell',
    'easier said than done', 'better late than never', 'crystal clear',
    'the bottom line', 'reinvent the wheel', 'back to the drawing board',
    'bite the bullet', 'break the ice', 'hit the nail on the head',
    'under the weather', 'add insult to injury', 'beat around the bush',
    'between a rock and a hard place', 'bread and butter', 'cutting edge',
    'every cloud has a silver lining', 'go the extra mile', 'in the long run',
    'once in a blue moon', 'piece of cake', 'read between the lines',
    'the whole nine yards', 'time will tell', 'up in the air',
    'writing on the wall', 'you can say that again',
]
text = open('$file').read().lower()
found = 0
for c in CLICHES:
    hits = [(m.start(), m.end()) for m in re.finditer(re.escape(c), text)]
    if hits:
        for start, end in hits:
            line = text[:start].count('\n') + 1
            print(f'  line {line}: \"{c}\"')
            found += 1
if found == 0:
    print('No cliches detected.')
else:
    print(f'\n{found} cliche(s) found.')
"
}

# filterwords -- find fiction filter words
filterwords() {
    typeset file="${1:?Usage: filterwords <file>}"
    python3 -c "
import re, sys
FILTERS = [
    'just', 'really', 'very', 'quite', 'rather', 'somewhat', 'suddenly',
    'immediately', 'finally', 'actually', 'basically', 'literally', 'simply',
    'definitely', 'certainly', 'absolutely', 'completely', 'totally', 'utterly',
    'entirely', 'practically', 'virtually', 'nearly', 'almost', 'somehow',
    'seemingly', 'apparently', 'perhaps', 'maybe', 'probably', 'possibly',
    'started to', 'began to', 'seemed to', 'appeared to', 'managed to',
    'was able to', 'could see', 'could hear', 'could feel', 'noticed that',
    'realized that', 'wondered if', 'thought about', 'decided to',
]
text = open('$file').read()
words_total = len(text.split())
text_lower = text.lower()
print(f'Filter words in {words_total} total words:\n')
found = []
for fw in FILTERS:
    count = len(re.findall(r'\b' + re.escape(fw) + r'\b', text_lower))
    if count > 0:
        found.append((count, fw))
found.sort(reverse=True)
total = 0
for count, fw in found:
    pct = count / words_total * 100
    print(f'  {fw:<20s} {count:>4d}  ({pct:.2f}%)')
    total += count
if found:
    print(f'\nTotal filter words: {total} ({total/words_total*100:.2f}%)')
else:
    print('No filter words found.')
"
}

# adverbs -- adverb frequency analysis (-ly words)
adverbs() {
    typeset file="${1:?Usage: adverbs <file>}"
    python3 -c "
import re
from collections import Counter
text = open('$file').read()
words = text.split()
total = len(words)
ly_words = [w.lower().strip('.,;:!?\"()[]') for w in words if re.match(r'.*ly[.,;:!?\"\)\]]*$', w.lower())]
exclude = {'only', 'early', 'family', 'holy', 'likely', 'lonely', 'lovely', 'ugly',
           'friendly', 'daily', 'monthly', 'weekly', 'yearly', 'ally', 'belly',
           'bully', 'fly', 'jelly', 'july', 'lily', 'rally', 'reply', 'supply', 'tally'}
ly_words = [w for w in ly_words if w not in exclude]
counts = Counter(ly_words)
print(f'Adverbs (-ly) in {total} total words:\n')
for word, count in counts.most_common(40):
    pct = count / total * 100
    print(f'  {word:<25s} {count:>3d}  ({pct:.2f}%)')
print(f'\nTotal -ly adverbs: {len(ly_words)} ({len(ly_words)/total*100:.2f}%)')
"
}

# saidbookisms -- find overused dialogue tags
saidbookisms() {
    typeset file="${1:?Usage: saidbookisms <file>}"
    python3 -c "
import re
from collections import Counter
TAGS = [
    'exclaimed', 'declared', 'proclaimed', 'announced', 'remarked', 'stated',
    'uttered', 'articulated', 'vocalized', 'opined', 'interjected', 'retorted',
    'quipped', 'snapped', 'barked', 'growled', 'hissed', 'snarled', 'sneered',
    'bellowed', 'thundered', 'boomed', 'roared', 'shrieked', 'screamed',
    'whimpered', 'sobbed', 'wailed', 'moaned', 'groaned', 'sighed', 'gasped',
    'breathed', 'purred', 'cooed', 'crooned', 'sang', 'chanted', 'intoned',
    'drawled', 'stammered', 'stuttered', 'mumbled', 'muttered', 'murmured',
    'whispered', 'pleaded', 'begged', 'demanded', 'commanded', 'ordered',
    'insisted', 'urged', 'cautioned', 'warned', 'admonished', 'chided',
    'scolded', 'lectured', 'preached', 'pontificated', 'mused', 'pondered',
    'reflected', 'observed', 'noted', 'acknowledged', 'conceded', 'admitted',
    'confessed', 'revealed', 'disclosed', 'divulged', 'volunteered',
    'suggested', 'proposed', 'offered', 'ventured', 'speculated',
]
text = open('$file').read().lower()
found = []
for tag in TAGS:
    count = len(re.findall(r'\b' + tag + r'\b', text))
    if count > 0:
        found.append((count, tag))
found.sort(reverse=True)
said_count = len(re.findall(r'\bsaid\b', text))
asked_count = len(re.findall(r'\basked\b', text))
print(f'Dialogue tags:')
print(f'  {\"said\":<25s} {said_count:>3d}  (standard)')
print(f'  {\"asked\":<25s} {asked_count:>3d}  (standard)')
if found:
    print()
    for count, tag in found:
        print(f'  {tag:<25s} {count:>3d}  (bookism)')
    total_bookisms = sum(c for c, _ in found)
    print(f'\nTotal said-bookisms: {total_bookisms}')
else:
    print('\nNo said-bookisms found.')
"
}

# overused -- find repeated phrases (n-grams) in a file
overused() {
    typeset file="${1:?Usage: overused <file> [n]}"
    typeset n="${2:-3}"
    python3 -c "
import re
from collections import Counter
text = open('$file').read().lower()
words = re.findall(r'[a-z]+', text)
n = int('$n')
ngrams = [' '.join(words[i:i+n]) for i in range(len(words)-n+1)]
counts = Counter(ngrams)
print(f'{n}-grams appearing 3+ times:\n')
found = False
for phrase, count in counts.most_common(50):
    if count < 3:
        break
    print(f'  {count:>3d}x  {phrase}')
    found = True
if not found:
    print('No repeated {}-grams found (3+ occurrences).'.format(n))
"
}

# ngrams -- general n-gram frequency analysis
ngrams() {
    typeset file="${1:?Usage: ngrams <file> [n] [top]}"
    typeset n="${2:-2}"
    typeset top="${3:-30}"
    python3 -c "
import re
from collections import Counter
text = open('$file').read().lower()
words = re.findall(r'[a-z]+', text)
n = int('$n')
top = int('$top')
grams = [' '.join(words[i:i+n]) for i in range(len(words)-n+1)]
counts = Counter(grams)
print(f'Top {top} {n}-grams:\n')
for phrase, count in counts.most_common(top):
    print(f'  {count:>4d}  {phrase}')
"
}

# concordance -- KWIC concordance for a word in a file
concordance() {
    typeset word="${1:?Usage: concordance <word> <file> [context-words]}"
    typeset file="${2:?Usage: concordance <word> <file> [context-words]}"
    typeset ctx="${3:-6}"
    python3 -c "
import re
text = open('$file').read()
words = text.split()
target = '$word'.lower()
ctx = int('$ctx')
for i, w in enumerate(words):
    if re.sub(r'[^a-z]', '', w.lower()) == target:
        left = ' '.join(words[max(0,i-ctx):i])
        right = ' '.join(words[i+1:i+ctx+1])
        print(f'{left:>50s}  [{w}]  {right}')
"
}

# collocations -- statistically significant word pairs
collocations() {
    typeset file="${1:?Usage: collocations <file> [top]}"
    typeset top="${2:-25}"
    python3 -c "
import re, math
from collections import Counter
text = open('$file').read().lower()
words = re.findall(r'[a-z]+', text)
N = len(words)
top = int('$top')
stop = {'the','a','an','and','or','but','in','on','at','to','for','of','with','by',
        'from','is','it','this','that','was','are','were','be','been','being','have',
        'has','had','do','does','did','will','would','shall','should','may','might',
        'can','could','i','he','she','we','they','you','my','his','her','our','their',
        'its','not','no','as','if','so','than'}
bigrams = [(words[i], words[i+1]) for i in range(N-1)
           if words[i] not in stop and words[i+1] not in stop
           and len(words[i]) > 2 and len(words[i+1]) > 2]
bg_counts = Counter(bigrams)
word_counts = Counter(words)
scored = []
for (w1, w2), count in bg_counts.items():
    if count < 2:
        continue
    pmi = math.log2((count * N) / (word_counts[w1] * word_counts[w2]))
    scored.append((pmi, count, f'{w1} {w2}'))
scored.sort(reverse=True)
print(f'Top {top} collocations (by PMI, min 2 occurrences):\n')
for pmi, count, phrase in scored[:top]:
    print(f'  {count:>3d}x  {pmi:>6.2f}  {phrase}')
"
}

# hapax -- count words used exactly once (hapax legomena)
hapax() {
    typeset file="${1:?Usage: hapax <file>}"
    python3 -c "
import re
from collections import Counter
text = open('$file').read().lower()
words = re.findall(r'[a-z]+', text)
counts = Counter(words)
hapax = [w for w, c in counts.items() if c == 1]
total = len(words)
types = len(counts)
print(f'Words: {total}  Types: {types}  Hapax legomena: {len(hapax)}')
print(f'Hapax ratio: {len(hapax)/types:.4f} (of types)  {len(hapax)/total:.4f} (of tokens)')
print(f'\nSample hapax (first 30):')
for w in sorted(hapax)[:30]:
    print(f'  {w}')
"
}

# vocab -- rich vocabulary analysis (TTR + Yule's K + hapax)
vocab() {
    typeset file="${1:?Usage: vocab <file>}"
    python3 -c "
import re, math
from collections import Counter
text = open('$file').read().lower()
words = re.findall(r'[a-z]+', text)
N = len(words)
counts = Counter(words)
types = len(counts)
hapax = sum(1 for c in counts.values() if c == 1)
dis = sum(1 for c in counts.values() if c == 2)
ttr = types / N if N else 0
freq_spectrum = Counter(counts.values())
M = sum(i * i * vi for i, vi in freq_spectrum.items())
K = 10000 * (M - N) / (N * N) if N > 1 else 0
W = N ** (types ** -0.172) if types else 0
R = 100 * math.log(N) / (1 - hapax/types) if types and hapax != types else 0
print(f'Vocabulary richness for: $file')
print(f'  Tokens (N):        {N:>8d}')
print(f'  Types (V):         {types:>8d}')
print(f'  Hapax legomena:    {hapax:>8d}')
print(f'  Dis legomena:      {dis:>8d}')
print(f'  TTR (V/N):         {ttr:>8.4f}')
print(f'  Yule K:            {K:>8.2f}  (lower = richer)')
print(f'  Brunet W:          {W:>8.2f}  (lower = richer)')
print(f'  Honore R:          {R:>8.2f}  (higher = richer)')
"
}

# corpuscompare -- compare word frequencies against a reference corpus
corpuscompare() {
    typeset file="${1:?Usage: corpuscompare <file> [top]}"
    typeset top="${2:-30}"
    python3 -c "
import re
from collections import Counter
from wordfreq import zipf_frequency
text = open('$file').read().lower()
words = re.findall(r'[a-z]+', text)
counts = Counter(words)
total = len(words)
top = int('$top')
scored = []
for word, count in counts.items():
    if count < 2 or len(word) < 3:
        continue
    doc_freq = count / total
    corpus_freq = 10 ** (zipf_frequency(word, 'en') - 9)
    if corpus_freq == 0:
        corpus_freq = 1e-9
    ratio = doc_freq / corpus_freq
    scored.append((ratio, count, word, zipf_frequency(word, 'en')))
scored.sort(reverse=True)
print(f'Words overrepresented vs. general English (top {top}):\n')
print(f'  {\"WORD\":<20s} {\"COUNT\":>5s} {\"RATIO\":>7s} {\"ZIPF\":>5s}')
for ratio, count, word, zipf in scored[:top]:
    print(f'  {word:<20s} {count:>5d} {ratio:>7.1f}x  {zipf:.1f}')
print(f'\nRatio = your frequency / corpus frequency. High = distinctive vocabulary.')
" 2>/dev/null || echo "pip install wordfreq"
}

# --- TYPOGRAPHY ---

# smartquotes -- fix straight quotes to curly
smartquotes() {
    typeset file="${1:?Usage: smartquotes <file>}"
    python3 -c "
import re, sys
text = open('$file').read()
text = re.sub(r'\"(\S)', r'\u201c\1', text)
text = re.sub(r'(\S)\"', r'\1\u201d', text)
text = re.sub(r'\"', r'\u201d', text)
text = re.sub(r\"(\s)'(\S)\", r'\\1\u2018\\2', text)
text = re.sub(r\"^'(\S)\", r'\u2018\\1', text)
text = re.sub(r\"'\", r'\u2019', text)
text = text.replace('---', '\u2014')
text = text.replace('--', '\u2013')
text = text.replace('...', '\u2026')
sys.stdout.write(text)
"
}

# unicodenorm -- Unicode NFC normalization
unicodenorm() {
    typeset file="${1:?Usage: unicodenorm <file>}"
    python3 -c "
import unicodedata, sys
text = open('$file').read()
normalized = unicodedata.normalize('NFC', text)
changes = sum(1 for a, b in zip(text, normalized) if a != b)
sys.stdout.write(normalized)
import sys as s
print(f'\n--- {changes} character(s) normalized', file=s.stderr)
"
}

# --- WRITING SESSIONS ---

# sprint -- timed writing sprint with before/after word count
sprint() {
    typeset file="${1:?Usage: sprint <file> [minutes]}"
    typeset minutes="${2:-25}"
    typeset start_wc
    start_wc=$(wc -w < "$file" 2>/dev/null || echo 0)
    echo "Sprint: $minutes minutes on $file"
    echo "Starting word count: $start_wc"
    echo "Timer starts NOW. Press Ctrl-C when done or wait for alarm."
    sleep $((minutes * 60)) && echo -e "\a\a\aTIME'S UP!" &
    typeset timer_pid=$!
    trap "kill $timer_pid 2>/dev/null" INT
    wait $timer_pid 2>/dev/null
    typeset end_wc
    end_wc=$(wc -w < "$file" 2>/dev/null || echo 0)
    typeset diff=$((end_wc - start_wc))
    typeset wpm=$((diff / minutes))
    echo ""
    echo "Ending word count: $end_wc"
    echo "Words written: $diff"
    echo "Words per minute: $wpm"
}

# tts -- pipe a file through espeak for proofreading by ear
tts() {
    typeset file="${1:?Usage: tts <file> [speed]}"
    typeset speed="${2:-160}"
    espeak-ng -s "$speed" -f "$file"
}

# tkcheck -- find TK/TODO/XXX/FIXME markers
tkcheck() {
    typeset target="${1:-.}"
    if [ -f "$target" ]; then
        grep -n -i --color=always '\bTK\b\|TODO\|XXX\|FIXME\|\bTBD\b' "$target" || echo "No markers found."
    else
        grep -rn -i --color=always '\bTK\b\|TODO\|XXX\|FIXME\|\bTBD\b' "$target" --include='*.md' --include='*.txt' --include='*.mdx' || echo "No markers found."
    fi
}

# deadline -- words remaining to a deadline at a daily rate
deadline() {
    typeset target="${1:?Usage: deadline <target-words> <current-words> <days-left>}"
    typeset current="${2:?Usage: deadline <target-words> <current-words> <days-left>}"
    typeset days="${3:?Usage: deadline <target-words> <current-words> <days-left>}"
    typeset remaining=$((target - current))
    typeset daily=$((remaining / days))
    echo "Target:    $target words"
    echo "Current:   $current words"
    echo "Remaining: $remaining words"
    echo "Days left: $days"
    echo "Daily rate: $daily words/day"
    if [ "$days" -le 7 ]; then
        echo "WARNING: tight deadline"
    fi
}

# scenestats -- chapter/scene word counts for fiction
scenestats() {
    typeset dir="${1:-.}"
    python3 -c "
import os, re, glob
directory = '$dir'
files = sorted(glob.glob(os.path.join(directory, '*.md')) + glob.glob(os.path.join(directory, '*.mdx')) + glob.glob(os.path.join(directory, '*.txt')))
if not files:
    print('No .md/.mdx/.txt files found.')
    exit()
total = 0
print(f'{\"FILE\":<40s} {\"WORDS\":>7s} {\"SCENES\":>7s}')
print('-' * 60)
for f in files:
    text = open(f).read()
    words = len(text.split())
    scenes = len(re.findall(r'^#{1,3}\s|^---\s*$|^\*\s*\*\s*\*', text, re.MULTILINE))
    scenes = max(scenes, 1)
    name = os.path.basename(f)
    print(f'{name:<40s} {words:>7d} {scenes:>7d}')
    total += words
print('-' * 60)
print(f'{\"TOTAL\":<40s} {total:>7d}')
"
}

# lintall -- run all prose linters in sequence
lintall() {
    typeset file="${1:?Usage: lintall <file>}"
    echo "========== VALE =========="
    vale "$file" 2>/dev/null || echo "(vale not available)"
    echo ""
    echo "========== PROSELINT =========="
    proselint "$file" 2>/dev/null || echo "(proselint not available)"
    echo ""
    echo "========== WRITE-GOOD =========="
    write-good "$file" 2>/dev/null || echo "(write-good not available)"
    echo ""
    echo "========== ALEX =========="
    alex "$file" 2>/dev/null || echo "(alex not available)"
    echo ""
    echo "========== DICTION =========="
    diction "$file" 2>/dev/null || echo "(diction not available)"
    echo ""
    echo "========== LANGUAGETOOL =========="
    if command -v languagetool-commandline &>/dev/null; then
        languagetool-commandline -l en-US "$file" 2>/dev/null
    elif [ -f /usr/share/java/languagetool/languagetool-commandline.jar ]; then
        java -jar /usr/share/java/languagetool/languagetool-commandline.jar -l en-US "$file" 2>/dev/null
    else
        echo "(languagetool not available)"
    fi
}