# 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
}