From 8b572480cc3690738540d20a225b555a4396c033 Mon Sep 17 00:00:00 2001 From: Richard Neumann Date: Tue, 22 Dec 2020 11:57:47 +0100 Subject: [PATCH] Added settings saving to RCON GUI. --- files/rcongui | 2 +- rcon/gui.py | 132 ++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 112 insertions(+), 22 deletions(-) diff --git a/files/rcongui b/files/rcongui index 1d39f3e..83ad9ed 100644 --- a/files/rcongui +++ b/files/rcongui @@ -1,5 +1,5 @@ #! /usr/bin/env python3 -"""An RCON client.""" +"""A minimalistic, GTK-based RCON GUI.""" from rcon.gui import main diff --git a/rcon/gui.py b/rcon/gui.py index ab837ae..6ec495d 100644 --- a/rcon/gui.py +++ b/rcon/gui.py @@ -1,12 +1,18 @@ """GTK based GUI.""" +from json import dump, load +from logging import basicConfig, getLogger +from os import getenv, name +from pathlib import Path from socket import timeout +from typing import NamedTuple from gi import require_version require_version('Gtk', '3.0') # pylint: disable=C0413 from gi.repository import Gtk +from rcon.config import LOG_FORMAT, LOGGER from rcon.exceptions import RequestIdMismatch, WrongPassword from rcon.proto import Client @@ -14,7 +20,28 @@ from rcon.proto import Client __all__ = ['main'] -class GUI(Gtk.Window): +if name == 'posix': + CACHE_DIR = Path.home().joinpath('.cache') +elif name == 'nt': + CACHE_DIR = Path(getenv('TEMP') or getenv('TMP')) +else: + raise NotImplementedError('Unsupported operating system.') + + +CACHE_FILE = CACHE_DIR.joinpath('rcongui.json') +LOGGER = getLogger('rcongui') + + +class RCONParams(NamedTuple): + """Represents the RCON parameters.""" + + host: str + port: int + passwd: str + command: str + + +class GUI(Gtk.Window): # pylint: disable=R0902 """A GTK based GUI for RCON.""" def __init__(self): @@ -36,6 +63,7 @@ class GUI(Gtk.Window): self.passwd = Gtk.Entry() self.passwd.set_placeholder_text('Password') + self.passwd.set_visibility(False) self.grid.attach(self.passwd, 2, 0, 1, 1) self.command = Gtk.Entry() @@ -48,7 +76,58 @@ class GUI(Gtk.Window): self.result = Gtk.Entry() self.result.set_placeholder_text('Result') - self.grid.attach(self.result, 0, 2, 3, 1) + self.grid.attach(self.result, 0, 2, 2, 1) + + self.savepw = Gtk.CheckButton(label='Save password') + self.grid.attach(self.savepw, 2, 2, 1, 1) + + self.load_gui_settings() + + @property + def gui_settings(self) -> dict: + """Returns the GUI settings as a dict.""" + json = { + 'host': self.host.get_text(), + 'port': self.port.get_text(), + 'command': self.command.get_text(), + 'result': self.result.get_text(), + 'savepw': (savepw := self.savepw.get_active()) + } + + if savepw: + json['passwd'] = self.passwd.get_text() + + return json + + @gui_settings.setter + def gui_settings(self, json: dict): + """Sets the GUI settings.""" + self.host.set_text(json.get('host', '')) + self.port.set_text(json.get('port', '')) + self.passwd.set_text(json.get('passwd', '')) + self.command.set_text(json.get('command', '')) + self.result.set_text(json.get('result', '')) + self.savepw.set_active(json.get('savepw', False)) + + def load_gui_settings(self) -> dict: + """Loads the GUI settings from the cache file.""" + try: + with CACHE_FILE.open('r') as cache: + self.gui_settings = load(cache) + except FileNotFoundError: + LOGGER.warning('Cache file not found: %s', CACHE_FILE) + except PermissionError: + LOGGER.error('Insufficient permissions to read: %s', CACHE_FILE) + except ValueError: + LOGGER.error('Cache file contains garbage: %s', CACHE_FILE) + + def save_gui_settings(self): + """Saves the GUI settings to the cache file.""" + try: + with CACHE_FILE.open('w') as cache: + dump(self.gui_settings, cache) + except PermissionError: + LOGGER.error('Insufficient permissions to read: %s', CACHE_FILE) def show_error(self, message: str): """Shows an error message.""" @@ -60,31 +139,40 @@ class GUI(Gtk.Window): message_dialog.run() message_dialog.destroy() - def on_button_clicked(self, _): - """Runs the client.""" + def get_rcon_params(self) -> RCONParams: + """Returns all settings as a dict.""" if not (host := self.host.get_text()): # pylint: disable=C0325 - self.show_error('No host specified.') - return + raise ValueError('No host specified.') if not (port := self.port.get_text()): # pylint: disable=C0325 - self.show_error('No port specified.') - return + raise ValueError('No port specified.') + + if not (command := self.command.get_text()): # pylint: disable=C0325 + raise ValueError('No command entered.') try: port = int(port) except ValueError: - self.show_error(f'Invalid port: {port}') - return + raise ValueError('Invalid port specified.') from None - passwd = self.passwd.get_text() + if not 0 <= port <= 65535: + raise ValueError('Invalid port specified.') - if not (command := self.command.get_text()): # pylint: disable=C0325 - self.show_error('No command entered.') - return + return RCONParams(host, port, self.passwd.get_text(), command) + + def run_rcon(self) -> str: + """Returns the current RCON settings.""" + params = self.get_rcon_params() + + with Client(params.host, params.port, passwd=params.passwd) as client: + return client.run(params.command) + def on_button_clicked(self, _): + """Runs the client.""" try: - with Client(host, port, passwd=passwd) as client: - result = client.run(command) + result = self.run_rcon() + except ValueError as error: + self.show_error(' '.join(error.args)) except ConnectionRefusedError: self.show_error('Connection refused.') except timeout: @@ -96,15 +184,17 @@ class GUI(Gtk.Window): else: self.result.set_text(result) + def terminate(self, *args, **kwargs): + """Saves the settings and terminates the application.""" + self.save_gui_settings() + Gtk.main_quit(*args, **kwargs) + def main(): """Starts the GUI.""" + basicConfig(format=LOG_FORMAT) win = GUI() - win.connect('destroy', Gtk.main_quit) + win.connect('destroy', win.terminate) win.show_all() Gtk.main() - - -if __name__ == '__main__': - main()