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 from socket import socket, timeout, AF_INET, SOCK_STREAM
  27 from sys import argv, exit, stderr
  28 from typing import Tuple
  29 
  30 
  31 info = '''
  32 def [words...]
  33 
  34 Lookup words from an online english dictionary at https://dict.org
  35 '''
  36 
  37 # no args or a leading help-option arg means show the help message and quit
  38 if len(argv) == 1 or argv[1] in ('-h', '--h', '-help', '--help'):
  39     print(info.strip(), file=stderr)
  40     exit(0)
  41 
  42 
  43 def slurp(conn: socket, chunksize: int = 1024) -> Tuple[bytes, bool]:
  44     '''Read the whole dict-format response to a previously-sent query.'''
  45 
  46     chunks = []
  47     while True:
  48         try:
  49             chunk = conn.recv(chunksize)
  50         except timeout:
  51             return bytes().join(chunks), True
  52         except Exception as e:
  53             return bytes(str(e)), False
  54 
  55         chunks.append(chunk)
  56         if not chunk or b'\n221 bye' in chunk:
  57             return bytes().join(chunks), True
  58 
  59 
  60 def lookup(what: str, timeout: float = 0.25) -> Tuple[str, bool]:
  61     '''Handle the whole request-response process for a word lookup.'''
  62 
  63     conn = socket(AF_INET, SOCK_STREAM)
  64     conn.connect(('dict.org', 2628))
  65     conn.sendall(bytes(f'define wn {what}\r\n', 'utf-8'))
  66     conn.settimeout(timeout)
  67 
  68     try:
  69         resp, ok = slurp(conn)
  70         s = resp.decode('utf-8').replace('\r\n', '\n')
  71         return s, ok
  72     except:
  73         return '', False
  74     finally:
  75         conn.close()
  76 
  77 
  78 def show(s: str) -> bool:
  79     '''
  80     Show response lines by color-coding them. This func returns false when
  81     any error-like metadata lines are detected.
  82     '''
  83 
  84     ok = True
  85 
  86     for line in s.split('\n'):
  87         if line == '':
  88             print()
  89             continue
  90 
  91         # metadata lines start with a numeric digit
  92         first = line[0]
  93 
  94         # prevent looked-up numbers from showing as metadata
  95         if ' ' not in line:
  96             print(line)
  97             continue
  98 
  99         # code 151 has the specific source for the match, so
 100         # use a unique style for that kind of metadata line
 101         if line.startswith('151 '):
 102             print('\x1b[38;5;4m', end='')
 103             print(line, end='')
 104             print('\x1b[0m')
 105             continue
 106 
 107         # gray out ok-type metadata lines
 108         if first == '1' or first == '2' or line == '.':
 109             print('\x1b[38;5;244m', end='')
 110             print(line, end='')
 111             print('\x1b[0m')
 112             continue
 113 
 114         # make error-type metadata lines stand out
 115         if first == '3' or first == '4' or first == '5':
 116             ok = False
 117             print('\x1b[38;5;124m', end='')
 118             print(line, end='')
 119             print('\x1b[0m')
 120             continue
 121 
 122         # keep all other lines unstyled
 123         print(line)
 124 
 125     return ok
 126 
 127 
 128 def help() -> None:
 129     '''Show a help message.'''
 130 
 131     out = stderr
 132     print('def [words...]', file=out)
 133     print(file=out)
 134     msg = 'Lookup words from an online english dictionary at https://dict.org'
 135     print(msg, file=out)
 136 
 137 
 138 # just show a help message when given no words to lookup
 139 if len(argv) < 2:
 140     help()
 141     exit(0)
 142 
 143 # handle explicit help options
 144 if len(argv) > 1 and argv[1] in ('-h', '-help', '--h', '--help'):
 145         help()
 146         exit(0)
 147 
 148 # don't show a styled title line when given only 1 word to lookup
 149 if len(argv) == 2:
 150     res, ok1 = lookup(argv[1])
 151     ok2 = show(res)
 152     exit(0 if ok1 and ok2 else 1)
 153 
 154 # handle multiple words to lookup by preceding each with styled title lines
 155 nerr = 0
 156 for i, word in enumerate(argv[1:]):
 157     if i > 0:
 158         print()
 159     print(f'\x1b[7m{word:80}\x1b[0m')
 160     (res, ok1) = lookup(word)
 161     ok2 = show(res)
 162     if not ok1 or not ok2:
 163         nerr += 1
 164 
 165 # fail the script if any lookup failed
 166 if nerr > 0:
 167     exit(1)