File: def.py
   1 #!/usr/bin/python3
   2 
   3 # The MIT License (MIT)
   4 #
   5 # Copyright © 2024 pacman64
   6 #
   7 # Permission is hereby granted, free of charge, to any person obtaining a copy
   8 # of this software and associated documentation files (the “Software”), to deal
   9 # in the Software without restriction, including without limitation the rights
  10 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  11 # copies of the Software, and to permit persons to whom the Software is
  12 # furnished to do so, subject to the following conditions:
  13 #
  14 # The above copyright notice and this permission notice shall be included in
  15 # all copies or substantial portions of the Software.
  16 #
  17 # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  19 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  20 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  21 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  22 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  23 # SOFTWARE.
  24 
  25 
  26 # def [words...]
  27 #
  28 # Lookup words from an online english dictionary at https://dict.org
  29 
  30 
  31 from socket import socket, timeout, AF_INET, SOCK_STREAM
  32 from sys import argv, exit, stderr, stdout
  33 
  34 
  35 def slurp(conn: socket, chunksize: int = 1024) -> (bytes, bool):
  36     '''Read the whole dict-format response to a previously-sent query.'''
  37 
  38     chunks = []
  39     while True:
  40         try:
  41             chunk = conn.recv(chunksize)
  42         except timeout:
  43             return bytes().join(chunks), True
  44         except Exception as e:
  45             return bytes(str(e)), False
  46 
  47         chunks.append(chunk)
  48         if not chunk or b'\n221 bye' in chunk:
  49             return bytes().join(chunks), True
  50 
  51 
  52 def lookup(what: str, timeout: float = 0.25) -> (str, bool):
  53     '''Handle the whole request-response process for a word lookup.'''
  54 
  55     conn = socket(AF_INET, SOCK_STREAM)
  56     conn.connect(('dict.org', 2628))
  57     conn.sendall(bytes(f'define wn {what}\r\n', 'utf-8'))
  58     conn.settimeout(timeout)
  59 
  60     try:
  61         resp, ok = slurp(conn)
  62         s = resp.decode('utf-8').replace('\r\n', '\n')
  63         return s, ok
  64     except:
  65         return '', False
  66     finally:
  67         conn.close()
  68 
  69 
  70 def show(s: str) -> bool:
  71     '''
  72     Show response lines by color-coding them. This func returns false when
  73     any error-like metadata lines are detected.
  74     '''
  75 
  76     ok = True
  77 
  78     for line in s.split('\n'):
  79         if line == '':
  80             print()
  81             continue
  82 
  83         # metadata lines start with a numeric digit
  84         first = line[0]
  85 
  86         # prevent looked-up numbers from showing as metadata
  87         if ' ' not in line:
  88             print(line)
  89             continue
  90 
  91         # code 151 has the specific source for the match, so
  92         # use a unique style for that kind of metadata line
  93         if line.startswith('151 '):
  94             print('\x1b[38;5;4m', end='')
  95             print(line, end='')
  96             print('\x1b[0m')
  97             continue
  98 
  99         # gray out ok-type metadata lines
 100         if first == '1' or first == '2' or line == '.':
 101             print('\x1b[38;5;244m', end='')
 102             print(line, end='')
 103             print('\x1b[0m')
 104             continue
 105 
 106         # make error-type metadata lines stand out
 107         if first == '3' or first == '4' or first == '5':
 108             ok = False
 109             print('\x1b[38;5;124m', end='')
 110             print(line, end='')
 111             print('\x1b[0m')
 112             continue
 113 
 114         # keep all other lines unstyled
 115         print(line)
 116 
 117     return ok
 118 
 119 
 120 def help() -> None:
 121     '''Show a help message.'''
 122 
 123     out = stderr
 124     print('def [words...]', file=out)
 125     print(file=out)
 126     msg = 'Lookup words from an online english dictionary at https://dict.org'
 127     print(msg, file=out)
 128 
 129 
 130 # just show a help message when given no words to lookup
 131 if len(argv) < 2:
 132     help()
 133     exit(0)
 134 
 135 # handle explicit help options
 136 if len(argv) > 1 and argv[1] in ('-h', '-help', '--h', '--help'):
 137         help()
 138         exit(0)
 139 
 140 stdout.reconfigure(newline='\n', encoding='utf-8')
 141 
 142 # don't show a styled title line when given only 1 word to lookup
 143 if len(argv) == 2:
 144     res, ok1 = lookup(argv[1])
 145     ok2 = show(res)
 146     exit(0 if ok1 and ok2 else 1)
 147 
 148 # handle multiple words to lookup by preceding each with styled title lines
 149 nerr = 0
 150 for i, word in enumerate(argv[1:]):
 151     if i > 0:
 152         print()
 153     print(f'\x1b[7m{word:80}\x1b[0m')
 154     (res, ok1) = lookup(word)
 155     ok2 = show(res)
 156     if not ok1 or not ok2:
 157         nerr += 1
 158 
 159 # fail the script if any lookup failed
 160 if nerr > 0:
 161     exit(1)