#!/usr/bin/env -S python3 -B
#
#    ft8con.py
#
#    WSJT-X text monitor.
#
#    Copyright (C) 2019,2023,2024 by Matt Roberts.
#    License: GNU GPL3 (www.gnu.org)
#
#

# the ALL file
AllDotTxt = '~/.local/share/WSJT-X/ALL.TXT'

# the BLOCKED file
BlockedTxt = '~/.local/share/WSJT-X/BLOCKED.TXT'

# the local callsign (use -m option to change this)
MyCall = 'N0CALL'

# color pairs
WHITE_ON_CLEAR = 1
YELLOW_ON_CLEAR = 2
GREEN_ON_CLEAR = 3
RED_ON_CLEAR = 4
BLUE_ON_CLEAR = 5
BLACK_ON_WHITE = 6
BLACK_ON_YELLOW = 7
BLACK_ON_GREEN = 8
BLACK_ON_RED = 9
BLACK_ON_BLUE = 10
WHITE_ON_BLUE = 11

# single-panel mode
SinglePanel = False

# number of history lines in waterfall
waterfall_lines = 1 # only support one for now

# waterfall edges
waterfall_fmin = 200 # Hz
waterfall_fmax = 3000 # Hz

# spots older than this are not shown
waterfall_age = 20 # slots

# spots older than this are shown, but with dimmer video
waterfall_old = 16 # slots

# enable exception display
ShowExceptions = True #False

# status style options
StatusIsBold = True
StatusIsReverse = False

# spot style
SpotsAreBold = False
CqColor = WHITE_ON_CLEAR
TxColor = YELLOW_ON_CLEAR
MeColor = GREEN_ON_CLEAR
BkColor = RED_ON_CLEAR
RxColor = WHITE_ON_CLEAR

#
#  global state
#

# width of left and right panels
left_width = 0
right_width = 0

# line counters
MonitorLines = 0
QsoLines = 0

# blocked calls
blocked = [ ]


# system modules
import sys
import os
import os.path
import time
import curses
import threading
import getopt
from datetime import datetime

# local modules
from tailall import tailall
from blocked import *


#
#  def read_rc() - read the rc file
#
def read_rc():
	global MyCall
	rc_path = os.path.expanduser('~/.ft8conrc')
	try:
		with io.open(rc_path, 'r') as rc:
			c = rc.readline()
			if c: c = c.strip()
			if c:
				c = c.split()
				if len(c) == 2:
					if (c[0].upper() == 'MYCALL'):
						MyCall = c[1]
					#
					#  TODO: other future settings go here
					#
	except:
		pass


#
#  def write_rc() - write the rc file
#
def write_rc():
	global MyCall
	rc_path = os.path.expanduser('~/.ft8conrc')
	try:
		if os.path.exists(rc_path):
			os.rename(rc_path, rc_path + '.bak')
		with io.open(rc_path, 'w') as rc:
			rc.write("MYCALL %s\n" % MyCall)
			#
			#  TODO: other future settings go here
			#
	except:
		pass


#
#  slot_width(...) in Hz
#
def slot_width(mode):
	if mode == 'FT4':
		return 60
	if mode == 'FT8':
		return 50
	return 100
	

#
#  slot_length(...) in sec
#
def slot_length(mode):
	if mode == 'FT4':
		return 7.5
	if mode == 'FT8':
		return 15
	return 60


#
#  watch_thread(...)
#
class watch_thread(threading.Thread):
	#
	#  set_show_dt(...)
	#
	def set_show_dt(self, sdt):
		global QsoLines

		self.show_dt = True if sdt else False
		if QsoLines:
			self.qsowin.addstr('\n')
		QsoLines += 1
		self.qsowin.addstr("Show Delta-T now %s" % self.show_dt)
		self.qsowin.refresh()

	#
	#  set_show_snr(...)
	#
	def set_show_snr(self, ssnr):
		global QsoLines

		self.show_snr = True if ssnr else False
		if QsoLines:
			self.qsowin.addstr('\n')
		QsoLines += 1
		self.qsowin.addstr("Show SNR now %s" % self.show_snr)
		self.qsowin.refresh()

	#
	#  set_cq_only(...)
	#
	def set_cq_only(self, cq_only):
		global QsoLines

		self.cq_only = True if cq_only else False
		if QsoLines:
			self.qsowin.addstr('\n')
		QsoLines += 1
		self.qsowin.addstr("CQ-only now %s" % self.cq_only)
		self.qsowin.refresh()
	
	#
	#  show_mycall(...)
	#
	def show_mycall(self):
		global QsoLines

		if QsoLines:
			self.qsowin.addstr('\n')
		QsoLines += 1
		self.qsowin.addstr("My call is %s" % MyCall)
		self.qsowin.refresh()

	#
	#  status_update
	#
	def status_update(self):
		global ShowExceptions
		global QsoLines, MonitorLines
		global StatusIsBold, StatusIsReverse

		with self.lock:
			try:
				y, x = self.stdscr.getmaxyx()
				now = datetime.utcnow()

				# compute the new status text
				statxt = " - RF %s MHz - AF %d Hz - %s - %02d:%02d:%02d Z - " % (
					self.freq, self.tx_audio, self.mode, now.hour, now.minute, now.second)
				if len(statxt) >= x:
					statxt = " - RF %s MHz - %s - %02d:%02d:%02d Z - " % (
						self.freq, self.mode, now.hour, now.minute, now.second)

				# only update if necessary
				if statxt != self.last_stat:
					self.last_stat = statxt

					# compute L/R padding
					pad1 = ' ' * int((x - len(statxt)) / 2)
					#pad2 = ' ' * (x - len(statxt) - len(pad1) - 1)

					# figure out the style information
					style = curses.A_BOLD if StatusIsBold else 0
					color = BLACK_ON_WHITE if StatusIsReverse else WHITE_ON_CLEAR
					last_tx_ago = time.time() - self.tx_time
					slot_len = slot_length(self.mode)
					if last_tx_ago <= (slot_len - 2):
						# color for TX
						color = BLACK_ON_YELLOW if StatusIsReverse else YELLOW_ON_CLEAR
					elif last_tx_ago <= (2 * slot_len):
						# color for RX immediately after TX
						color = BLACK_ON_GREEN if StatusIsReverse else GREEN_ON_CLEAR

					# enable bold if configured
					style = style | curses.color_pair(color)

					# draw
					#self.stawin.addstr(0, 0, pad1 + statxt + pad2, style)
					self.stawin.addstr(0, 0, pad1 + statxt, style)
					self.stawin.clrtoeol()
					self.stawin.refresh()

			except Exception as ex:
				if ShowExceptions:
					if QsoLines:
						self.qsowin.addstr('\n')
					QsoLines += 1
					self.qsowin.addstr("STA: " + str(ex))
					self.qsowin.refresh()

	#
	#  waterfall_legend
	#
	def waterfall_legend(self):
		global ShowExceptions
		global MonitorLines, QsoLines

		with self.lock:
			try:
				y, x = self.stdscr.getmaxyx()
				step = (waterfall_fmax - waterfall_fmin) / x
				wfcolor = curses.color_pair(WHITE_ON_CLEAR)
				self.wfwin.addstr(1, 0, "^" + str(waterfall_fmin), wfcolor)
				self.wfwin.clrtoeol()
				self.wfwin.addstr(1, x - 6, str(waterfall_fmax) + "^", wfcolor)
				start = 500
				tab = 500
				if x < 70:
					start = 1000
					tab = 1000
				for f in [ start * i for i in range(1, int(waterfall_fmax / tab)) ]:
					self.wfwin.addstr(1, int((f - waterfall_fmin) / step), "^" + str(f), wfcolor)
			except Exception as ex:
				if ShowExceptions:
					if QsoLines:
						self.qsowin.addstr('\n')
					QsoLines += 1
					self.qsowin.addstr("WFL: " + str(ex))
					self.qsowin.refresh()

	#
	#  waterfall_update
	#
	def waterfall_update(self):
		global ShowExceptions
		global MonitorLines, QsoLines

		with self.lock:
			# don't redraw if no changes
			if not self.spots_dirty:
				return
			self.spots_dirty = False

			try:
				now = time.time()
				y, x = self.stdscr.getmaxyx()
				step = (waterfall_fmax - waterfall_fmin) / x
				chunk = int(slot_width(self.mode) / step)
				if chunk < 1:
					chunk = 1
				cols = [ ]

				# clear the line
				self.wfwin.move(0, 0)
				self.wfwin.clrtoeol()

				# calculate the spot columns and age
				toMe = [ ]

				# sort by SNR, so that the higher SNR show on top of lower ones
				thespots = sorted(self.spots, key = lambda s: s['snr'])

				# for each spot...
				for spot in thespots:
					col = int((int(spot['audio']) - waterfall_fmin) / step)
					if col < 0 or col >= x:
						continue
					age = now - spot['when']
					snr = int(spot['snr'])
					if MyCall.upper() in spot['what'] and age <= slot_length(self.mode):
						toMe.append(col)
					elif age < (waterfall_age * slot_length(self.mode)):
						cols.append((col, age, snr))

				# then draw them
				for item in cols:
					coloridx = 0
					if item[1] >= (waterfall_old) * slot_length(self.mode): # old spots
						if item[2] >= 0:
							coloridx = GREEN_ON_CLEAR
						elif item[2] >= -10:
							coloridx = WHITE_ON_CLEAR
						elif item[2] >= -20:
							coloridx = YELLOW_ON_CLEAR
						else:
							coloridx = RED_ON_CLEAR
					else: # new spot
						if item[2] >= 0:
							coloridx = BLACK_ON_GREEN
						elif item[2] >= -10:
							coloridx = BLACK_ON_WHITE
						elif item[2] >= -20:
							coloridx = BLACK_ON_YELLOW
						else:
							coloridx = BLACK_ON_RED
					for i in range(chunk):
						sx = item[0] + i  # the X coordinate
						if sx < x:
							self.wfwin.addstr(0, sx, '#', curses.color_pair(coloridx))

				# draw special color for people calling me
				for col in toMe:
					for i in range(chunk):
						sx = col + i  # the X coordinate
						if sx < x:
							self.wfwin.addstr(0, sx, '$', curses.color_pair(BLACK_ON_GREEN))

				# draw the TX caret
				if now - self.last_tx <= slot_length(self.mode):
					col = int((int(self.tx_audio) - waterfall_fmin) / step)
					sym = '^' if col in toMe else '+'
					for i in range(chunk):
						sx = col + i  # the X coordinate
						if sx < x:
							self.wfwin.addstr(0, col + i, sym, curses.color_pair(WHITE_ON_BLUE))

				# redraw the legend
				self.waterfall_legend()
					
				# refresh the window
				self.wfwin.refresh()

			except Exception as ex:
				if ShowExceptions:
					if QsoLines:
						self.qsowin.addstr('\n')
					QsoLines += 1
					self.qsowin.addstr("WFU: " + str(ex))
					self.qsowin.refresh()

	#
	#  callback
	#
	def callback(self, d):
		global ShowExceptions, SinglePanel
		global MonitorLines, QsoLines
		global SpotsAreBold, MeColor, BkColor, CqColor, TxColor, RxColor
		global blocked
		global right_width

		line = None
		with self.lock:
			try:
				mon_dirty = False
				qso_dirty = False
				wf_dirty = False

				# update the CQ table
				if d['cq'] and d['from']:
					self.cqs[d['from']] = d

				# fetch the mode and frequency
				self.mode = d['mode']
				self.freq = d['freq']

				# if the mode or freq have changed, clear the QSO window
				if self.mode != self.oldmode or self.freq != self.oldfreq:
					self.qsowin.clear()
					QsoLines = 0
					qso_dirty = True

					if not SinglePanel:
						self.monwin.clear()
						MonitorLines = 0
						mon_dirty = True

					self.wfwin.move(0, 0)
					self.wfwin.clrtoeol()
					self.spots = [ ]
					wf_dirty = True

				# store the old freq and mode for next comparison
				self.oldmode = self.mode
				self.oldfreq = self.freq

				# form the reception report
				dtx = '*' if (abs(float(d['dt'])) > 0.3) else ' '
				snr = ' ' if d['tx'] else d['snr']
				if snr and not d['tx'] and snr[0] != '-':
					snr = '+' + snr
				what = (' '.join(d['what'])).strip()

				# trim decoder reliability reports
				idx = what.find(' ?')
				if idx > 0:
					what = what[:idx].strip()
				if what.endswith('a3') or what.endswith('a2') or what.endswith('a1'):
					what = what[:-2].strip()

				# format the line, start with time
				line = "%s " % d['time']

				# add SNR and DT if enabled
				if self.show_snr:
					if self.show_dt:
						line += "%3s " % snr
					else:
						line += "%3s%1s " % (snr, dtx)
				if self.show_dt:
					dtstr = "     " if d['tx'] or len(d['dt']) == 0 else "%0.1f " % float(d['dt'])
					if dtstr[0] not in ' -':
						dtstr = '+' + dtstr
					line += dtstr

				# finish with AF and message
				line += "%4s  %s" % (d['audio'], what)

				is_cq = False
				for i in [ ' CQ ', ' CQDX ', ' CQFD ' ]:
					if i in line:
						is_cq = True
						break

				# add some left-padding in single-panel mode, if there is space
				if SinglePanel:
					pad = right_width - 40
					if pad > 2: pad = 2
					if pad < 0: pad = 0
					if pad:
						line = (' ' * pad) + line

				# TX update to QSO window
				if d['tx'] == True:
					# trim the line if needed to prevent word wrap
					to_draw = line
					if len(to_draw) > right_width:
						to_draw = to_draw[:right_width]

					self.last_cq = (' CQ ' in to_draw)
					self.last_tx = time.time()
					TX_QSO_WINDOW = curses.color_pair(TxColor)
					if SpotsAreBold:
						TX_QSO_WINDOW = TX_QSO_WINDOW | curses.A_BOLD
					if QsoLines:
						self.qsowin.addstr('\n')
					QsoLines += 1
					self.qsowin.addstr(to_draw, TX_QSO_WINDOW)
					self.tx_audio = int(d['audio'])
					self.tx_time = int(time.time())
					self.spots_dirty = True
					qso_dirty = True
				# RX update to one or both windows
				else:
					now = time.time()
					d['when'] = now
					self.spots.append(d)
					self.spots_dirty = True
					for spot in self.spots:
						if (now - spot['when']) >= (waterfall_age * slot_length(self.mode)):
							self.spots.remove(spot)

					# QSO window spot update
					my_upper = MyCall.upper()
					if my_upper in d['what']:
						#
						#  look for call in blocked list
						#
						#  TODO: extract the base call, and make sure it's a call
						#
						suppress = False
						for i in d['what']:
							if i in blocked:
								# skip blocked calls in QSO window
								suppress = True
								break

						# trim the line if needed to prevent word wrap
						to_draw = line
						if len(to_draw) >= right_width:
							to_draw = to_draw[:right_width - 1]

						if QsoLines:
							self.qsowin.addstr('\n')
						QsoLines += 1
						RX_QSO_WINDOW = curses.color_pair(BkColor if suppress else MeColor)
						if SpotsAreBold:
							RX_QSO_WINDOW = RX_QSO_WINDOW | curses.A_BOLD
						self.qsowin.addstr(to_draw, RX_QSO_WINDOW)
						qso_dirty = True

					# monitor window update
					else:
						# default is normal text
						pair = curses.color_pair(RxColor)

						# clobber detection
						clobber = False
						if self.tx_audio and self.last_tx and self.last_cq:
							offset = abs(self.tx_audio - int(d['audio']))
							if (offset < slot_width(self.mode)) and (time.time() - self.last_tx <= (4.0 * slot_width(self.mode))):
								pair = curses.color_pair(RED_ON_CLEAR)
								if SpotsAreBold:
									pair = pair | curses.A_BOLD
								clobber = True

						# CQ detection
						if not clobber and 'CQ' in d['what']:
							pair = curses.color_pair(CqColor)
							if SpotsAreBold:
								pair = pair | curses.A_BOLD

						# trim the line if needed to prevent word wrap
						to_draw = line
						if len(to_draw) >= left_width:
							to_draw = to_draw[:left_width - 1]

						# draw CQ lines in the monitor window
						if not SinglePanel:
							if is_cq or not self.cq_only:
								if MonitorLines:
									self.monwin.addstr('\n')
								MonitorLines += 1
								self.monwin.addstr(to_draw, pair)
							mon_dirty = True

				# update the spot windows
				if mon_dirty and not SinglePanel:
					self.monwin.refresh()
				if qso_dirty:
					self.qsowin.refresh()
				if wf_dirty:
					self.wfwin.refresh()

				# status update
				self.status_update()

			except Exception as ex:
				if ShowExceptions:
					if QsoLines:
						self.qsowin.addstr('\n')
					QsoLines += 1
					self.qsowin.addstr('CB: ' + str(ex))
					self.qsowin.refresh()


	#
	#  worker
	#
	def worker(self):
		tailall(AllDotTxt, self.callback)


	#
	#  ctor
	#
	def __init__(self, stdscr, monwin, qsowin, stawin, wfwin):
		super().__init__(target=self.worker, daemon=True)
		self.stdscr = stdscr
		self.monwin = monwin
		self.qsowin = qsowin
		self.stawin = stawin
		self.wfwin = wfwin
		self.lock = threading.RLock()
		self.tx_audio = 0
		self.tx_time = 0
		self.last_tx = 0
		self.last_cq = False
		self.mode = "None"
		self.freq = "0.0"
		self.oldmode = "None"
		self.oldfreq = "0.0"
		self.spots = [ ]
		self.spots_dirty = False
		self.last_stat = ""
		self.cq_only = False
		self.show_snr = True
		self.show_dt = True
		self.cqs = { }


#
#  initialize()
#
def initialize(stdscr):
	stdscr.clear()
	curses.start_color()
	curses.use_default_colors()
	curses.cbreak()
	curses.noecho()
	curses.curs_set(0)
	stdscr.keypad(True)
	stdscr.notimeout(True)
	curses.halfdelay(2) # 0.2s

	# normal colors
	curses.init_pair(WHITE_ON_CLEAR, curses.COLOR_WHITE, -1) # w/bk
	curses.init_pair(YELLOW_ON_CLEAR, curses.COLOR_YELLOW, -1) # y/bk
	curses.init_pair(GREEN_ON_CLEAR, curses.COLOR_GREEN, -1) # g/bk
	curses.init_pair(RED_ON_CLEAR, curses.COLOR_RED, -1) # r/bk
	curses.init_pair(BLUE_ON_CLEAR, curses.COLOR_BLUE, -1) # r/bk
	# inverted colors
	curses.init_pair(BLACK_ON_WHITE, curses.COLOR_BLACK, curses.COLOR_WHITE) # bk/w
	curses.init_pair(BLACK_ON_YELLOW, curses.COLOR_BLACK, curses.COLOR_YELLOW) # bk/y
	curses.init_pair(BLACK_ON_GREEN, curses.COLOR_BLACK, curses.COLOR_GREEN) # bk/g
	curses.init_pair(BLACK_ON_RED, curses.COLOR_BLACK, curses.COLOR_RED) # bk/r
	curses.init_pair(BLACK_ON_BLUE, curses.COLOR_BLACK, curses.COLOR_BLUE) # bk/bl
	curses.init_pair(WHITE_ON_BLUE, curses.COLOR_WHITE, curses.COLOR_BLUE) # wh/bl


#
#  make_wins()
#
def make_wins(stdscr):
	global left_width
	global right_width
	global ShowExceptions, SinglePanel

	monwin = None
	qsowin = None
	stawin = None
	wfwin = None
	try:
		y, x = stdscr.getmaxyx()

		# lines consumed by header and footer
		hf_lines = 1 + waterfall_lines + 1

		# the MONITOR window
		if not SinglePanel:
			left_width = int(x / 2)
			monwin = curses.newwin(y - hf_lines, left_width, 1, 0)
			monwin.scrollok(True)
			monwin.clear()
			#monwin.notimeout(True)

		#
		#  NOTE:  curses.newwin(nlines, ncols, begin_y, begin_x)
		#

		# the QSO window
		right_width = int(x) if SinglePanel else int(x / 2) - 1
		right_origin = 0 if SinglePanel else int(x / 2) + 1
		qsowin = curses.newwin(y - hf_lines, right_width, 1, right_origin)
		qsowin.scrollok(True)
		qsowin.clear()
		#qsowin.notimeout(True)

		# the STATUS window
		stawin = curses.newwin(1, x, 0, 0)
		stawin.scrollok(False)
		stawin.clear()
		#stawin.notimeout(True)

		# the WATERFALL window
		wfwin = curses.newwin(1 + waterfall_lines, x, y - (waterfall_lines + 1), 0)
		wfwin.scrollok(False) # we'll place things manually on this one
		wfwin.clear()

	except Exception as ex:
		if ShowExceptions:
			qsowin.addstr("MKW: " + str(ex))
			qsowin.refresh()

	# return the set
	return monwin, qsowin, stawin, wfwin


#
#  force_redraw()
#
def force_redraw(monwin, qsowin, stawin, wfwin):
	stawin.redrawwin()
	stawin.refresh()
	if not SinglePanel:
		monwin.redrawwin()
		monwin.refresh()
	qsowin.redrawwin()
	qsowin.refresh()
	wfwin.redrawwin()
	wfwin.refresh()


#
#  main()
#
def main(stdscr):
	global MyCall
	global QsoLines, MonitorLines
	global blocked
	global AllDotTxt, BlockedTxt, SinglePanel
	global left_width, right_width
	global ShowExceptions

	# read the rc file if it exists
	read_rc()

	# read command line
	show_snr = True
	show_dt = False
	cq_only = False
	optlist, cmdline = getopt.getopt(sys.argv[1:], '1a:b:hm:qst')
	for opt in optlist:
		if opt[0] == '-1':      # single-panel mode
			SinglePanel = True
		elif opt[0] == '-a':    # provide ALL.TXT path
			AllDotTxt = opt[1]
		elif opt[0] == '-b':    # provide BLOCKED.TXT path
			BlockedTxt = opt[1]
		elif opt[0] == '-h':    # usage information (help)
			return 1
		elif opt[0] == '-m':    # change MyCall
			MyCall = opt[1].upper()
		elif opt[0] == '-q':    # show only CQ in pane 1
			cq_only = True
		elif opt[0] == '-s':    # hide SNR columns
			show_snr = False
		elif opt[0] == '-t':    # show SNR columns
			show_dt = True

	# perform tilde expansion on file paths
	AllDotTxt = os.path.expanduser(AllDotTxt)
	BlockedTxt = os.path.expanduser(BlockedTxt)

	# setup
	initialize(stdscr)
	monwin, qsowin, stawin, wfwin = make_wins(stdscr)

	# run the background thread to watch the ALL.TXT and update the windows
	w = watch_thread(stdscr, monwin, qsowin, stawin, wfwin)
	w.set_cq_only(cq_only)			
	w.set_show_snr(show_snr)
	w.set_show_dt(show_dt)
	w.waterfall_legend()
	w.show_mycall()
	w.start()

	# tickle the update logic (TODO: this shouldn't be necessary)
	ch = stdscr.getch()
	force_redraw(monwin, qsowin, stawin, wfwin)

	# loop vars
	last_wf = 0 # last waterfall timer refresh
	last_bt = 0 # last BLOCKED.TXT change


	# wait for a keypress
	while True:
		# status update
		w.status_update()

		# waterfall update
		w.waterfall_update()

		# read key input
		ch = None
		try:
			ch = stdscr.getch()
			if ch == curses.ERR:
				#monwin.addstr("DEBUG: Update cycle.\n")
				#monwin.refresh()

				# update waterfall now and then
				now = time.time()
				if now - last_wf > 5:
					last_wf = now
					w.spots_dirty = True

				# also update blocked call list
				mtime = get_blocked_age(BlockedTxt)
				if mtime > last_bt:
					last_bt = mtime
					new_blocked = get_blocked_calls(BlockedTxt)
					if MyCall.upper() in new_blocked:
						new_blocked.remove(MyCall.upper())
					blocked = new_blocked
					#monwin.addstr("DEBUG: Loaded %d blocks.\n" % len(blocked))
					#monwin.refresh()

				continue
		except TimeoutError as ex:
			# no key available
			continue

		# handle keystroke
		if ch == curses.KEY_RESIZE: #  ** SIGWINCH **
			y, x = stdscr.getmaxyx()
			if x > 0 and y > 0:
				# lines consumed by header and footer
				hf_lines = 1 + waterfall_lines + 1

				left_width = int(x / 2)
				right_width = x if SinglePanel else int(x / 2) - 1

				if not SinglePanel:
					monwin.resize(y - hf_lines, left_width)
				qsowin.resize(y - hf_lines, right_width)
				stawin.resize(1, x)
				wfwin.resize(1 + waterfall_lines, x)

				if not SinglePanel:
					monwin.mvwin(1, 0)
				new_x = 0 if SinglePanel else int(x / 2) + 1
				qsowin.mvwin(1, new_x)
				stawin.mvwin(0, 0)
				wfwin.mvwin(y - (1 + waterfall_lines), 0)

				# redraw the waterfall
				w.waterfall_update()

				# redraw the legend
				w.waterfall_legend()

				stdscr.clear()
				stdscr.refresh()
				force_redraw(monwin, qsowin, stawin, wfwin)
		elif ch == ord('a'):
			cq_only = not cq_only
			w.set_cq_only(cq_only)			
		elif ch == ord('c'):
			qsowin.clear()
			QsoLines = 0
			force_redraw(monwin, qsowin, stawin, wfwin)
		elif ch == ord('C'):
			monwin.clear()
			MonitorLines = 0
			force_redraw(monwin, qsowin, stawin, wfwin)
		elif ch == ord('K'):
			stdscr.clear()
			stdscr.refresh()
			force_redraw(monwin, qsowin, stawin, wfwin)
		elif ch == ord('q'):
			break
		elif ch == ord('r'):
			force_redraw(monwin, qsowin, stawin, wfwin)

	# shut down curses and exit
	curses.endwin()
	write_rc()
	sys.exit(0)


#
#  entry point
#
if __name__ == '__main__':
	result = curses.wrapper(main)
	if result == 1:
		sys.stderr.write("Usage: %s [-a ALL.TXT][-b BLOCKED.TXT][-m mycall][-1hqst]\n" % sys.argv[0])
		sys.stderr.write("       -1 - single-panel mode\n")
		sys.stderr.write("       -h - this help text\n")
		sys.stderr.write("       -q - show only CQ lines in left column\n")
		sys.stderr.write("       -s - disable SNR column\n")
		sys.stderr.write("       -t - enable delta-T column\n")

# EOF
