|
@ -1,12 +1,18 @@ |
|
|
"""GTK based GUI.""" |
|
|
"""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 socket import timeout |
|
|
|
|
|
from typing import NamedTuple |
|
|
|
|
|
|
|
|
from gi import require_version |
|
|
from gi import require_version |
|
|
require_version('Gtk', '3.0') |
|
|
require_version('Gtk', '3.0') |
|
|
# pylint: disable=C0413 |
|
|
# pylint: disable=C0413 |
|
|
from gi.repository import Gtk |
|
|
from gi.repository import Gtk |
|
|
|
|
|
|
|
|
|
|
|
from rcon.config import LOG_FORMAT, LOGGER |
|
|
from rcon.exceptions import RequestIdMismatch, WrongPassword |
|
|
from rcon.exceptions import RequestIdMismatch, WrongPassword |
|
|
from rcon.proto import Client |
|
|
from rcon.proto import Client |
|
|
|
|
|
|
|
@ -14,7 +20,28 @@ from rcon.proto import Client |
|
|
__all__ = ['main'] |
|
|
__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.""" |
|
|
"""A GTK based GUI for RCON.""" |
|
|
|
|
|
|
|
|
def __init__(self): |
|
|
def __init__(self): |
|
@ -36,6 +63,7 @@ class GUI(Gtk.Window): |
|
|
|
|
|
|
|
|
self.passwd = Gtk.Entry() |
|
|
self.passwd = Gtk.Entry() |
|
|
self.passwd.set_placeholder_text('Password') |
|
|
self.passwd.set_placeholder_text('Password') |
|
|
|
|
|
self.passwd.set_visibility(False) |
|
|
self.grid.attach(self.passwd, 2, 0, 1, 1) |
|
|
self.grid.attach(self.passwd, 2, 0, 1, 1) |
|
|
|
|
|
|
|
|
self.command = Gtk.Entry() |
|
|
self.command = Gtk.Entry() |
|
@ -48,7 +76,58 @@ class GUI(Gtk.Window): |
|
|
|
|
|
|
|
|
self.result = Gtk.Entry() |
|
|
self.result = Gtk.Entry() |
|
|
self.result.set_placeholder_text('Result') |
|
|
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): |
|
|
def show_error(self, message: str): |
|
|
"""Shows an error message.""" |
|
|
"""Shows an error message.""" |
|
@ -60,31 +139,40 @@ class GUI(Gtk.Window): |
|
|
message_dialog.run() |
|
|
message_dialog.run() |
|
|
message_dialog.destroy() |
|
|
message_dialog.destroy() |
|
|
|
|
|
|
|
|
def on_button_clicked(self, _): |
|
|
def get_rcon_params(self) -> RCONParams: |
|
|
"""Runs the client.""" |
|
|
"""Returns all settings as a dict.""" |
|
|
if not (host := self.host.get_text()): # pylint: disable=C0325 |
|
|
if not (host := self.host.get_text()): # pylint: disable=C0325 |
|
|
self.show_error('No host specified.') |
|
|
raise ValueError('No host specified.') |
|
|
return |
|
|
|
|
|
|
|
|
|
|
|
if not (port := self.port.get_text()): # pylint: disable=C0325 |
|
|
if not (port := self.port.get_text()): # pylint: disable=C0325 |
|
|
self.show_error('No port specified.') |
|
|
raise ValueError('No port specified.') |
|
|
return |
|
|
|
|
|
|
|
|
if not (command := self.command.get_text()): # pylint: disable=C0325 |
|
|
|
|
|
raise ValueError('No command entered.') |
|
|
|
|
|
|
|
|
try: |
|
|
try: |
|
|
port = int(port) |
|
|
port = int(port) |
|
|
except ValueError: |
|
|
except ValueError: |
|
|
self.show_error(f'Invalid port: {port}') |
|
|
raise ValueError('Invalid port specified.') from None |
|
|
return |
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
return RCONParams(host, port, self.passwd.get_text(), command) |
|
|
self.show_error('No command entered.') |
|
|
|
|
|
return |
|
|
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: |
|
|
try: |
|
|
with Client(host, port, passwd=passwd) as client: |
|
|
result = self.run_rcon() |
|
|
result = client.run(command) |
|
|
except ValueError as error: |
|
|
|
|
|
self.show_error(' '.join(error.args)) |
|
|
except ConnectionRefusedError: |
|
|
except ConnectionRefusedError: |
|
|
self.show_error('Connection refused.') |
|
|
self.show_error('Connection refused.') |
|
|
except timeout: |
|
|
except timeout: |
|
@ -96,15 +184,17 @@ class GUI(Gtk.Window): |
|
|
else: |
|
|
else: |
|
|
self.result.set_text(result) |
|
|
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(): |
|
|
def main(): |
|
|
"""Starts the GUI.""" |
|
|
"""Starts the GUI.""" |
|
|
|
|
|
|
|
|
|
|
|
basicConfig(format=LOG_FORMAT) |
|
|
win = GUI() |
|
|
win = GUI() |
|
|
win.connect('destroy', Gtk.main_quit) |
|
|
win.connect('destroy', win.terminate) |
|
|
win.show_all() |
|
|
win.show_all() |
|
|
Gtk.main() |
|
|
Gtk.main() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__': |
|
|
|
|
|
main() |
|
|
|
|
|