diff options
-rwxr-xr-x | twerk | 237 | ||||
-rwxr-xr-x | twerk.first | 122 |
2 files changed, 359 insertions, 0 deletions
diff --git a/twerk b/twerk new file mode 100755 index 0000000..2b0dfd7 --- /dev/null +++ b/twerk | |||
@@ -0,0 +1,237 @@ | |||
1 | #!/bin/sh | ||
2 | ## twerk: a twtxt client | ||
3 | # by Case Duckworth | ||
4 | |||
5 | ### Entry point | ||
6 | |||
7 | usage() { | ||
8 | cat >&2 <<EOF | ||
9 | TWERK: a twtxt client | ||
10 | usage: twerk [options] [file] | ||
11 | |||
12 | twerk can be fed a list of URLs from standard input or from a FILE. | ||
13 | Each line is expected to contain a URL of a twtxt feed and (optionally) the | ||
14 | display name to use for that feed. | ||
15 | |||
16 | options (default): | ||
17 | -h Show this help and exit. | ||
18 | -t Include the time in each post date. | ||
19 | -f Force redownload of twt data. | ||
20 | -n LIMIT Only show LIMIT posts (25). 0 means no limit. | ||
21 | -w WIDTH Limit the output's width to WIDTH characters (72). | ||
22 | -W WIDTH Limit the username's width to WIDTH characters (12). | ||
23 | -i INDENT Use INDENT spaces as a hanging indent for too-long lines | ||
24 | ([calculated]). | ||
25 | -p COMMAND Use COMMAND to post twts ([not set]). | ||
26 | This command will be fed a twtxt-formatted string via stdin. | ||
27 | Recommended: \`ssh <host> tee -a <file>\`. | ||
28 | |||
29 | parameters: | ||
30 | FILE List of twtxt feeds to fetch, one feed per line. | ||
31 | Optionally, a string after the feed URL will be its | ||
32 | display name. | ||
33 | EOF | ||
34 | exit "${1:-0}" | ||
35 | } | ||
36 | |||
37 | configure() { | ||
38 | # defaults | ||
39 | TWERK_WIDTH="${TWERK_WIDTH:-72}" | ||
40 | TWERK_LIMIT="${TWERK_LIMIT:-25}" | ||
41 | TWERK_INCLUDE_TIME="${TWERK_INCLUDE_TIME:-0}" | ||
42 | TWERK_USER_WIDTH="${TWERK_USER_WIDTH:-12}" | ||
43 | TWERK_HANG="${TWERK_HANG:-}" # empty to calculate off other variables | ||
44 | TWERK_FORCE="${TWERK_FORCE:-false}" | ||
45 | TWERK_POST_COMMAND="${TWERK_POST_COMMAND:-:}" # no-op | ||
46 | |||
47 | while getopts htfn:w:W:i:p:c: opt | ||
48 | do | ||
49 | case "$opt" in | ||
50 | h) usage ;; | ||
51 | t) TWERK_INCLUDE_TIME=1 ;; | ||
52 | f) TWERK_FORCE=true ;; | ||
53 | n) TWERK_LIMIT="$OPTARG" ;; | ||
54 | w) TWERK_WIDTH="$OPTARG" ;; | ||
55 | W) TWERK_USER_WIDTH="$OPTARG" ;; | ||
56 | i) TWERK_HANG="$OPTARG" ;; | ||
57 | p) TWERK_POST_COMMAND="$OPTARG" ;; | ||
58 | c) TWERK_FEEDS="$OPTARG" ;; | ||
59 | *) usage 1 ;; | ||
60 | esac | ||
61 | done | ||
62 | |||
63 | if test -z "$TWERK_HANG" | ||
64 | then | ||
65 | TWERK_HANG=$((TWERK_USER_WIDTH+(TWERK_INCLUDE_TIME*6)+10+3)) | ||
66 | echo >&2 "$TWERK_HANG" | ||
67 | fi | ||
68 | } | ||
69 | |||
70 | main() { | ||
71 | TWERK_CACHE="${XDG_CACHE_HOME:-$HOME/.cache}/twerk" | ||
72 | mkdir -p "$TWERK_CACHE" | ||
73 | TWERK_FILE="$TWERK_CACHE/:file:" | ||
74 | |||
75 | TWERK_LESSKEY="$TWERK_CACHE/:lesskey:" | ||
76 | lesskey_setup | ||
77 | |||
78 | if "$_TWERK_INITIAL" | ||
79 | then | ||
80 | configure "$@" | ||
81 | shift "$((OPTIND - 1))" | ||
82 | TWERK_FEEDS="$1" | ||
83 | fi | ||
84 | |||
85 | cat "$TWERK_FEEDS" | | ||
86 | fetch_posts | | ||
87 | sort_posts | | ||
88 | limit_posts "$TWERK_LIMIT" | | ||
89 | format_posts > "$TWERK_FILE" | ||
90 | |||
91 | read_posts "$TWERK_FILE" | ||
92 | } | ||
93 | |||
94 | ### Library | ||
95 | |||
96 | fetch_posts() { | ||
97 | while read -r url name | ||
98 | do | ||
99 | if test -z "$name" | ||
100 | then | ||
101 | file="$TWERK_CACHE/$(url_normalize "$name")" | ||
102 | else | ||
103 | file="$TWERK_CACHE/$name" | ||
104 | fi | ||
105 | |||
106 | if "$TWERK_FORCE" || test -z "$(find "$file" -prune -mtime -1 2>/dev/null)" | ||
107 | then | ||
108 | printf >&2 'Downloading %s...\n' "$url" | ||
109 | curl -sf "$url" | | ||
110 | filter_lines | | ||
111 | while read -r date post | ||
112 | do | ||
113 | printf '%s\t%s\t%s\t%s\n' \ | ||
114 | "$date" "$name" "$url" "$post" | ||
115 | done | | ||
116 | tee "$file" | ||
117 | else | ||
118 | cat "$file" | ||
119 | fi | ||
120 | done | ||
121 | } | ||
122 | |||
123 | url_normalize() { | ||
124 | printf '%s\n' "$1" | tr -d ':/~' | ||
125 | } | ||
126 | |||
127 | filter_lines() { | ||
128 | while read -r line | ||
129 | do | ||
130 | case "$line" in | ||
131 | \#*) ;; | ||
132 | '') ;; | ||
133 | *) printf '%s\n' "$line" ;; | ||
134 | esac | ||
135 | done | ||
136 | } | ||
137 | |||
138 | limit_posts() { | ||
139 | ln=0 | ||
140 | while read -r line | ||
141 | do | ||
142 | if test "$1" -ne 0 && test "$ln" -gt "$1" | ||
143 | then | ||
144 | break | ||
145 | fi | ||
146 | printf '%s\n' "$line" | ||
147 | ln=$((ln+1)) | ||
148 | done | ||
149 | } | ||
150 | |||
151 | sort_posts() { | ||
152 | sort -r | ||
153 | } | ||
154 | |||
155 | format_posts() { | ||
156 | while IFS=' ' read -r date name url post | ||
157 | do | ||
158 | if test 0 -eq "$TWERK_INCLUDE_TIME" | ||
159 | then | ||
160 | TWERK_DATE_WIDTH=10 | ||
161 | date="${date%T*}" | ||
162 | else | ||
163 | TWERK_DATE_WIDTH=16 | ||
164 | fi | ||
165 | |||
166 | printf "%${TWERK_DATE_WIDTH}s | %${TWERK_USER_WIDTH}s | " \ | ||
167 | "$(echo "$date" | head -c$TWERK_DATE_WIDTH)" \ | ||
168 | "$(echo "$name" | head -c$TWERK_USER_WIDTH)" | ||
169 | |||
170 | export url name date post TWERK_WIDTH TWERK_HANG | ||
171 | export linewidth="$TWERK_HANG" | ||
172 | |||
173 | printf '%s\n' "$post" | | ||
174 | tr ' ' '\n' | | ||
175 | while IFS= read word | ||
176 | do | ||
177 | if test $(( linewidth + ${#word} )) -ge "$TWERK_WIDTH" | ||
178 | then | ||
179 | echo | ||
180 | linewidth=0 | ||
181 | printf "%${TWERK_HANG}s \` " "" | ||
182 | linewidth=$((linewidth+TWERK_HANG+2)) | ||
183 | fi | ||
184 | printf '%s ' "$word" | ||
185 | linewidth=$((linewidth + ${#word})) | ||
186 | done | ||
187 | echo | ||
188 | done | ||
189 | } | ||
190 | |||
191 | |||
192 | lesskey_setup() { | ||
193 | # t will exit with code 48 | ||
194 | if ! test -r "$TWERK_LESSKEY" | ||
195 | then | ||
196 | lesskey -o "$TWERK_LESSKEY" - <<EOF | ||
197 | #command | ||
198 | t quit 0 | ||
199 | g quit 1 | ||
200 | #line-edit | ||
201 | #env | ||
202 | EOF | ||
203 | fi | ||
204 | } | ||
205 | |||
206 | read_posts() { | ||
207 | less \ | ||
208 | -k "$TWERK_LESSKEY" \ | ||
209 | -Pm'twerk! t\: twt, g\: refresh' -m \ | ||
210 | "$1" | ||
211 | |||
212 | case "$?" in | ||
213 | 0) exit ;; | ||
214 | 48) post ;; | ||
215 | 49) refresh ;; | ||
216 | esac | ||
217 | } | ||
218 | |||
219 | post() { | ||
220 | printf 'Twt: ' >&2 | ||
221 | read -r post | ||
222 | echo >&2 | ||
223 | |||
224 | post="$(printf '%s\t%s\n' "$(date +'%FT%T%z')" "$post")" | ||
225 | |||
226 | eval "printf '%s\n' \"\$post\" | $TWERK_POST_COMMAND" | ||
227 | _TWERK_INITIAL=false main "$TWERK_FEEDS" | ||
228 | } | ||
229 | |||
230 | refresh() { | ||
231 | TWERK_FORCE=true main "$TWERK_FEEDS" | ||
232 | } | ||
233 | |||
234 | ################################### | ||
235 | _TWERK_INITIAL=true | ||
236 | test -z "$DEBUG" || set -x | ||
237 | test -z "$SOURCE" && main "$@" | ||
diff --git a/twerk.first b/twerk.first new file mode 100755 index 0000000..9b43e3e --- /dev/null +++ b/twerk.first | |||
@@ -0,0 +1,122 @@ | |||
1 | #!/bin/sh | ||
2 | ## twerk: a twtxt client in sh | ||
3 | # by Case Duckworth | ||
4 | |||
5 | ### Entry point | ||
6 | |||
7 | usage() { | ||
8 | cat >&2 <<EOF | ||
9 | TWERK: a twtxt client | ||
10 | usage: twerk [options] [file] | ||
11 | |||
12 | options: | ||
13 | -h show this help and quit | ||
14 | -t include time in post date | ||
15 | -T don't include time in post date (default) | ||
16 | -n NUMBER limit posts to NUMBER (100) | ||
17 | -w WIDTH limit the width of the display (72) | ||
18 | -H WIDTH define the hanging indent of twts | ||
19 | -u WIDTH limit the width of the user display (12) | ||
20 | EOF | ||
21 | exit "${1:-0}" | ||
22 | } | ||
23 | |||
24 | configure() { | ||
25 | TWERK_INCLUDE_TIME=0 | ||
26 | TWERK_WIDTH="${COLUMNS:-72}" | ||
27 | TWERK_USER_WIDTH=12 | ||
28 | while getopts hn:w:H:t opt; do | ||
29 | case "$opt" in | ||
30 | h) usage ;; | ||
31 | n) TWERK_N="$OPTARG" ;; | ||
32 | w) TWERK_WIDTH="$OPTARG" ;; | ||
33 | H) TWERK_HANG="$OPTARG" ;; | ||
34 | u) TWERK_USER_WIDTH="$OPTARG" ;; | ||
35 | t) TWERK_INCLUDE_TIME=1 ;; | ||
36 | T) TWERK_INCLUDE_TIME=0 ;; | ||
37 | *) usage 1 ;; | ||
38 | esac | ||
39 | done | ||
40 | if test -z "$TWERK_HANG"; then | ||
41 | TWERK_HANG=$((TWERK_USER_WIDTH+(TWERK_INCLUDE_TIME*6)+10+3)) | ||
42 | fi | ||
43 | |||
44 | } | ||
45 | |||
46 | main() { | ||
47 | configure "$@"; shift "$((OPTIND - 1))" | ||
48 | cat "${@:--}" | | ||
49 | curl_from_file | ||
50 | } | ||
51 | |||
52 | ### Library | ||
53 | |||
54 | curl_from_file() { | ||
55 | while read -r url name; do | ||
56 | curl -sf "$url" | | ||
57 | filter_posts | | ||
58 | awk -v url="$url" -v name="$name" \ | ||
59 | '{ printf "%s\t%s\t%s\n", name, url, $0; }' | ||
60 | done | | ||
61 | sort_posts | | ||
62 | format_posts | ||
63 | } | ||
64 | |||
65 | filter_posts() { | ||
66 | filter_comments | limit_posts "${TWERK_N:-100}" | ||
67 | } | ||
68 | |||
69 | filter_comments() { | ||
70 | awk '/^[ \t]*#/{next;}/^$/{next;}{print;}' | ||
71 | } | ||
72 | |||
73 | limit_posts() { | ||
74 | head -n "$1" | ||
75 | } | ||
76 | |||
77 | |||
78 | sort_posts() { | ||
79 | sort -r | ||
80 | } | ||
81 | |||
82 | format_posts() { | ||
83 | awk \ | ||
84 | -v UW="$TWERK_USER_WIDTH" \ | ||
85 | -v IT="$TWERK_INCLUDE_TIME" \ | ||
86 | -v WIDTH="$TWERK_WIDTH" \ | ||
87 | -v HANG="$TWERK_HANG" \ | ||
88 | 'BEGIN {FS="\t";} | ||
89 | !IT { sub(/T.*$/, "", $3); } | ||
90 | IT { | ||
91 | sub(/+.*$/, "", $3); | ||
92 | sub(/T/, " ", $3); | ||
93 | sub(/:..$/,"",$3); | ||
94 | } | ||
95 | |||
96 | { printf("%s | %" UW "s | ", $3, substr($1,1,UW-1)); } | ||
97 | |||
98 | # wrap lines | ||
99 | { | ||
100 | w = HANG # width | ||
101 | ls = 0 # lines | ||
102 | split($2, words, /[ \t]/) | ||
103 | for (i=1; i<=length(words); i++) { | ||
104 | if (w+length(words[i]) >= WIDTH) { | ||
105 | print "" | ||
106 | w = 0 | ||
107 | if (++ls) { | ||
108 | printf "%" HANG "s ` ", "" | ||
109 | w += HANG + 2 | ||
110 | } | ||
111 | } | ||
112 | printf "%s ", words[i] | ||
113 | w += length(words[i]) | ||
114 | } | ||
115 | print "" | ||
116 | } | ||
117 | ' | ||
118 | } | ||
119 | |||
120 | ### | ||
121 | test -z "$DEBUG" || set -x | ||
122 | test -z "$SOURCE" && main "$@" | ||