mirror of
https://github.com/EDCD/EDMarketConnector.git
synced 2025-04-08 05:20:03 +03:00
Merge branch 'develop' into enhancement/1805/additional-prototyping
This commit is contained in:
commit
b74524a4cd
.gitignoreChangeLog.mdEDMC.pyEDMCLogging.pyEDMCSystemProfiler.pyEDMarketConnector.py
L10n
build.pycollate.pycommodity.pycompanion.pyconfig
coriolis-update-files.pydashboard.pydebug_webserver.pydocs/examples/plugintest
installer.pyl10n.pyloadout.pymonitor.pyplug.pyprefs.pyrequirements-dev.txtrequirements.txtscripts
ships.jsontheme.pyttkHyperlinkLabel.py
3
.gitignore
vendored
3
.gitignore
vendored
@ -9,8 +9,9 @@ build
|
||||
dist.win32/
|
||||
dist.*
|
||||
|
||||
# Ignore generated ChangeLog.html file
|
||||
# Ignore generated ChangeLog files
|
||||
ChangeLog.html
|
||||
/scripts/script_output
|
||||
|
||||
# Ignore files
|
||||
dump
|
||||
|
12
ChangeLog.md
12
ChangeLog.md
@ -6,6 +6,18 @@ This is the master changelog for Elite Dangerous Market Connector. Entries are
|
||||
in the source (not distributed with the Windows installer) for the
|
||||
currently used version.
|
||||
---
|
||||
Release 5.11.2
|
||||
===
|
||||
|
||||
This release fixes a bug where minimizing to the system tray could cause the program to not un-minimize.
|
||||
|
||||
**Changes and Enhancements**
|
||||
* Updated Translations
|
||||
* Added a developer utility to help speed up changelog development
|
||||
|
||||
**Bug Fixes**
|
||||
* Fixed a bug where minimizing to the system tray could cause the program to not un-minimize.
|
||||
|
||||
Release 5.11.1
|
||||
===
|
||||
|
||||
|
11
EDMC.py
11
EDMC.py
@ -14,6 +14,7 @@ import locale
|
||||
import os
|
||||
import queue
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from time import sleep, time
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
@ -212,22 +213,24 @@ def main(): # noqa: C901, CCR001
|
||||
# system, chances are its the current locale, and not utf-8. Otherwise if it was copied, its probably
|
||||
# utf8. Either way, try the system FIRST because reading something like cp1251 in UTF-8 results in garbage
|
||||
# but the reverse results in an exception.
|
||||
json_file = os.path.abspath(args.j)
|
||||
json_file = Path(args.j).resolve()
|
||||
try:
|
||||
with open(json_file) as file_handle:
|
||||
data = json.load(file_handle)
|
||||
except UnicodeDecodeError:
|
||||
with open(json_file, encoding='utf-8') as file_handle:
|
||||
data = json.load(file_handle)
|
||||
config.set('querytime', int(os.path.getmtime(args.j)))
|
||||
file_path = Path(args.j)
|
||||
modification_time = file_path.stat().st_mtime
|
||||
config.set('querytime', int(modification_time))
|
||||
|
||||
else:
|
||||
# Get state from latest Journal file
|
||||
logger.debug('Getting state from latest journal file')
|
||||
try:
|
||||
monitor.currentdir = config.get_str('journaldir', default=config.default_journal_dir)
|
||||
monitor.currentdir = Path(config.get_str('journaldir', default=config.default_journal_dir))
|
||||
if not monitor.currentdir:
|
||||
monitor.currentdir = config.default_journal_dir
|
||||
monitor.currentdir = config.default_journal_dir_path
|
||||
|
||||
logger.debug(f'logdir = "{monitor.currentdir}"')
|
||||
logfile = monitor.journal_newest_filename(monitor.currentdir)
|
||||
|
@ -26,12 +26,13 @@ To utilise logging in core code, or internal plugins, include this:
|
||||
|
||||
To utilise logging in a 'found' (third-party) plugin, include this:
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
import logging
|
||||
|
||||
plugin_name = os.path.basename(os.path.dirname(__file__))
|
||||
# Retrieve the name of the plugin folder
|
||||
plugin_name = Path(__file__).resolve().parent.name
|
||||
# Set up logger with hierarchical name including appname and plugin_name
|
||||
# plugin_name here *must* be the name of the folder the plugin resides in
|
||||
# See, plug.py:load_plugins()
|
||||
logger = logging.getLogger(f'{appname}.{plugin_name}')
|
||||
"""
|
||||
from __future__ import annotations
|
||||
@ -41,7 +42,6 @@ import logging
|
||||
import logging.handlers
|
||||
import os
|
||||
import pathlib
|
||||
import tempfile
|
||||
import warnings
|
||||
from contextlib import suppress
|
||||
from fnmatch import fnmatch
|
||||
@ -51,9 +51,7 @@ from threading import get_native_id as thread_native_id
|
||||
from time import gmtime
|
||||
from traceback import print_exc
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
import config as config_mod
|
||||
from config import appcmdname, appname, config
|
||||
from config import appcmdname, appname, config, trace_on
|
||||
|
||||
# TODO: Tests:
|
||||
#
|
||||
@ -104,7 +102,7 @@ warnings.simplefilter('default', DeprecationWarning)
|
||||
|
||||
|
||||
def _trace_if(self: logging.Logger, condition: str, message: str, *args, **kwargs) -> None:
|
||||
if any(fnmatch(condition, p) for p in config_mod.trace_on):
|
||||
if any(fnmatch(condition, p) for p in trace_on):
|
||||
self._log(logging.TRACE, message, args, **kwargs) # type: ignore # we added it
|
||||
return
|
||||
|
||||
@ -184,8 +182,7 @@ class Logger:
|
||||
# We want the files in %TEMP%\{appname}\ as {logger_name}-debug.log and
|
||||
# rotated versions.
|
||||
# This is {logger_name} so that EDMC.py logs to a different file.
|
||||
logfile_rotating = pathlib.Path(tempfile.gettempdir())
|
||||
logfile_rotating /= f'{appname}'
|
||||
logfile_rotating = pathlib.Path(config.app_dir_path / 'logs')
|
||||
logfile_rotating.mkdir(exist_ok=True)
|
||||
logfile_rotating /= f'{logger_name}-debug.log'
|
||||
|
||||
@ -489,8 +486,8 @@ class EDMCContextFilter(logging.Filter):
|
||||
:return: The munged module_name.
|
||||
"""
|
||||
file_name = pathlib.Path(frame_info.filename).expanduser()
|
||||
plugin_dir = pathlib.Path(config.plugin_dir_path).expanduser()
|
||||
internal_plugin_dir = pathlib.Path(config.internal_plugin_dir_path).expanduser()
|
||||
plugin_dir = config.plugin_dir_path.expanduser()
|
||||
internal_plugin_dir = config.internal_plugin_dir_path.expanduser()
|
||||
# Find the first parent called 'plugins'
|
||||
plugin_top = file_name
|
||||
while plugin_top and plugin_top.name != '':
|
||||
|
@ -11,7 +11,7 @@ import locale
|
||||
import webbrowser
|
||||
import platform
|
||||
import sys
|
||||
from os import chdir, environ, path
|
||||
from os import chdir, environ
|
||||
import pathlib
|
||||
import logging
|
||||
from journal_lock import JournalLock
|
||||
@ -19,10 +19,10 @@ from journal_lock import JournalLock
|
||||
if getattr(sys, "frozen", False):
|
||||
# Under py2exe sys.path[0] is the executable name
|
||||
if sys.platform == "win32":
|
||||
chdir(path.dirname(sys.path[0]))
|
||||
chdir(pathlib.Path(sys.path[0]).parent)
|
||||
# Allow executable to be invoked from any cwd
|
||||
environ["TCL_LIBRARY"] = path.join(path.dirname(sys.path[0]), "lib", "tcl")
|
||||
environ["TK_LIBRARY"] = path.join(path.dirname(sys.path[0]), "lib", "tk")
|
||||
environ['TCL_LIBRARY'] = str(pathlib.Path(sys.path[0]).parent / 'lib' / 'tcl')
|
||||
environ['TK_LIBRARY'] = str(pathlib.Path(sys.path[0]).parent / 'lib' / 'tk')
|
||||
|
||||
else:
|
||||
# We still want to *try* to have CWD be where the main script is, even if
|
||||
@ -44,11 +44,12 @@ def get_sys_report(config: config.AbstractConfig) -> str:
|
||||
plt = platform.uname()
|
||||
locale.setlocale(locale.LC_ALL, "")
|
||||
lcl = locale.getlocale()
|
||||
monitor.currentdir = config.get_str(
|
||||
monitor.currentdir = pathlib.Path(config.get_str(
|
||||
"journaldir", default=config.default_journal_dir
|
||||
)
|
||||
)
|
||||
if not monitor.currentdir:
|
||||
monitor.currentdir = config.default_journal_dir
|
||||
monitor.currentdir = config.default_journal_dir_path
|
||||
try:
|
||||
logfile = monitor.journal_newest_filename(monitor.currentdir)
|
||||
if logfile is None:
|
||||
@ -115,12 +116,12 @@ def main() -> None:
|
||||
root.withdraw() # Hide the window initially to calculate the dimensions
|
||||
try:
|
||||
icon_image = tk.PhotoImage(
|
||||
file=path.join(cur_config.respath_path, "io.edcd.EDMarketConnector.png")
|
||||
file=cur_config.respath_path / "io.edcd.EDMarketConnector.png"
|
||||
)
|
||||
|
||||
root.iconphoto(True, icon_image)
|
||||
except tk.TclError:
|
||||
root.iconbitmap(path.join(cur_config.respath_path, "EDMarketConnector.ico"))
|
||||
root.iconbitmap(cur_config.respath_path / "EDMarketConnector.ico")
|
||||
|
||||
sys_report = get_sys_report(cur_config)
|
||||
|
||||
|
@ -20,8 +20,7 @@ import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import webbrowser
|
||||
import tempfile
|
||||
from os import chdir, environ, path
|
||||
from os import chdir, environ
|
||||
from time import localtime, strftime, time
|
||||
from typing import TYPE_CHECKING, Any, Literal
|
||||
from constants import applongname, appname, protocolhandler_redirect
|
||||
@ -32,26 +31,29 @@ from constants import applongname, appname, protocolhandler_redirect
|
||||
if getattr(sys, 'frozen', False):
|
||||
# Under py2exe sys.path[0] is the executable name
|
||||
if sys.platform == 'win32':
|
||||
chdir(path.dirname(sys.path[0]))
|
||||
os.chdir(pathlib.Path(sys.path[0]).parent)
|
||||
# Allow executable to be invoked from any cwd
|
||||
environ['TCL_LIBRARY'] = path.join(path.dirname(sys.path[0]), 'lib', 'tcl')
|
||||
environ['TK_LIBRARY'] = path.join(path.dirname(sys.path[0]), 'lib', 'tk')
|
||||
environ['TCL_LIBRARY'] = str(pathlib.Path(sys.path[0]).parent / 'lib' / 'tcl')
|
||||
environ['TK_LIBRARY'] = str(pathlib.Path(sys.path[0]).parent / 'lib' / 'tk')
|
||||
|
||||
else:
|
||||
# We still want to *try* to have CWD be where the main script is, even if
|
||||
# not frozen.
|
||||
chdir(pathlib.Path(__file__).parent)
|
||||
|
||||
|
||||
# config will now cause an appname logger to be set up, so we need the
|
||||
# console redirect before this
|
||||
if __name__ == '__main__':
|
||||
# Keep this as the very first code run to be as sure as possible of no
|
||||
# output until after this redirect is done, if needed.
|
||||
if getattr(sys, 'frozen', False):
|
||||
from config import config
|
||||
# By default py2exe tries to write log to dirname(sys.executable) which fails when installed
|
||||
# unbuffered not allowed for text in python3, so use `1 for line buffering
|
||||
log_file_path = path.join(tempfile.gettempdir(), f'{appname}.log')
|
||||
log_file_path = pathlib.Path(config.app_dir_path / 'logs')
|
||||
log_file_path.mkdir(exist_ok=True)
|
||||
log_file_path /= f'{appname}.log'
|
||||
|
||||
sys.stdout = sys.stderr = open(log_file_path, mode='wt', buffering=1) # Do NOT use WITH here.
|
||||
# TODO: Test: Make *sure* this redirect is working, else py2exe is going to cause an exit popup
|
||||
|
||||
@ -439,7 +441,7 @@ class AppWindow:
|
||||
if sys.platform == 'win32':
|
||||
from simplesystray import SysTrayIcon
|
||||
|
||||
def open_window(systray: 'SysTrayIcon') -> None:
|
||||
def open_window(systray: 'SysTrayIcon', *args) -> None:
|
||||
self.w.deiconify()
|
||||
|
||||
menu_options = (("Open", None, open_window),)
|
||||
@ -453,8 +455,8 @@ class AppWindow:
|
||||
self.w.wm_iconbitmap(default='EDMarketConnector.ico')
|
||||
|
||||
else:
|
||||
self.w.tk.call('wm', 'iconphoto', self.w, '-default',
|
||||
tk.PhotoImage(file=path.join(config.respath_path, 'io.edcd.EDMarketConnector.png')))
|
||||
image_path = config.respath_path / 'io.edcd.EDMarketConnector.png'
|
||||
self.w.tk.call('wm', 'iconphoto', self.w, '-default', image=tk.PhotoImage(file=image_path))
|
||||
|
||||
# TODO: Export to files and merge from them in future ?
|
||||
self.theme_icon = tk.PhotoImage(
|
||||
@ -604,7 +606,7 @@ class AppWindow:
|
||||
self.help_menu.add_command(command=lambda: self.updater.check_for_updates()) # Check for Updates...
|
||||
# About E:D Market Connector
|
||||
self.help_menu.add_command(command=lambda: not self.HelpAbout.showing and self.HelpAbout(self.w))
|
||||
logfile_loc = pathlib.Path(tempfile.gettempdir()) / appname
|
||||
logfile_loc = pathlib.Path(config.app_dir_path / 'logs')
|
||||
self.help_menu.add_command(command=lambda: prefs.open_folder(logfile_loc)) # Open Log Folder
|
||||
self.help_menu.add_command(command=lambda: prefs.help_open_system_profiler(self)) # Open Log Folde
|
||||
|
||||
@ -831,9 +833,20 @@ class AppWindow:
|
||||
)
|
||||
update_msg = update_msg.replace('\\n', '\n')
|
||||
update_msg = update_msg.replace('\\r', '\r')
|
||||
stable_popup = tk.messagebox.askyesno(title=title, message=update_msg, parent=postargs.get('Parent'))
|
||||
stable_popup = tk.messagebox.askyesno(title=title, message=update_msg)
|
||||
if stable_popup:
|
||||
webbrowser.open("https://github.com/edCD/eDMarketConnector/releases/latest")
|
||||
webbrowser.open("https://github.com/EDCD/eDMarketConnector/releases/latest")
|
||||
if postargs.get('Restart_Req'):
|
||||
# LANG: Text of Notification Popup for EDMC Restart
|
||||
restart_msg = tr.tl('A restart of EDMC is required. EDMC will now restart.')
|
||||
restart_box = tk.messagebox.Message(
|
||||
title=tr.tl('Restart Required'), # LANG: Title of Notification Popup for EDMC Restart
|
||||
message=restart_msg,
|
||||
type=tk.messagebox.OK
|
||||
)
|
||||
restart_box.show()
|
||||
if restart_box:
|
||||
app.onexit(restart=True)
|
||||
|
||||
def set_labels(self):
|
||||
"""Set main window labels, e.g. after language change."""
|
||||
@ -1625,7 +1638,7 @@ class AppWindow:
|
||||
# Avoid file length limits if possible
|
||||
provider = config.get_str('shipyard_provider', default='EDSY')
|
||||
target = plug.invoke(provider, 'EDSY', 'shipyard_url', loadout, monitor.is_beta)
|
||||
file_name = path.join(config.app_dir_path, "last_shipyard.html")
|
||||
file_name = config.app_dir_path / "last_shipyard.html"
|
||||
|
||||
with open(file_name, 'w') as f:
|
||||
f.write(SHIPYARD_HTML_TEMPLATE.format(
|
||||
@ -1837,7 +1850,7 @@ class AppWindow:
|
||||
)
|
||||
exit_thread.start()
|
||||
|
||||
def onexit(self, event=None) -> None:
|
||||
def onexit(self, event=None, restart: bool = False) -> None:
|
||||
"""Application shutdown procedure."""
|
||||
if sys.platform == 'win32':
|
||||
shutdown_thread = threading.Thread(
|
||||
@ -1900,6 +1913,8 @@ class AppWindow:
|
||||
self.w.destroy()
|
||||
|
||||
logger.info('Done.')
|
||||
if restart:
|
||||
os.execv(sys.executable, ['python'] + sys.argv)
|
||||
|
||||
def drag_start(self, event) -> None:
|
||||
"""Initiate dragging the window."""
|
||||
|
@ -468,8 +468,11 @@
|
||||
/* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:908; */
|
||||
"Plugins folder" = "Plugins folder";
|
||||
|
||||
/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:915; */
|
||||
"Open" = "Open";
|
||||
/* prefs.py: Label on button used to open the Plugin Folder; */
|
||||
"Open Plugins Folder" = "Open Plugins Folder";
|
||||
|
||||
/* prefs.py: Selecting the Location of the Plugin Directory on the Filesystem; */
|
||||
"Plugin Directory Location" = "Plugin Directory Location";
|
||||
|
||||
/* prefs.py: Tip/label about how to disable plugins; In files: prefs.py:923; */
|
||||
"Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name" = "Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name";
|
||||
@ -804,12 +807,20 @@
|
||||
/* EDMarketConnector.py: Inform the user the Update Track has changed; */
|
||||
"Update Track Changed to {TRACK}" = "Update Track Changed to {TRACK}";
|
||||
|
||||
|
||||
/* EDMarketConnector.py: Inform User of Beta -> Stable Transition Risks; */
|
||||
"Update track changed to Stable from Beta. You will no longer receive Beta updates. You will stay on your current Beta version until the next Stable release.\r\n\r\nYou can manually revert to the latest Stable version. To do so, you must download and install the latest Stable version manually. Note that this may introduce bugs or break completely if downgrading between major versions with significant changes.\r\n\r\nDo you want to open GitHub to download the latest release?" = "Update track changed to Stable from Beta. You will no longer receive Beta updates. You will stay on your current Beta version until the next Stable release.\r\n\r\nYou can manually revert to the latest Stable version. To do so, you must download and install the latest Stable version manually. Note that this may introduce bugs or break completely if downgrading between major versions with significant changes.\r\n\r\nDo you want to open GitHub to download the latest release?";
|
||||
|
||||
/* EDMarketConnector.py: Title of Notification Popup for EDMC Restart; */
|
||||
"Restart Required" = "Restart Required";
|
||||
|
||||
/* EDMarketConnector.py: Text of Notification Popup for EDMC Restart; */
|
||||
"A restart of EDMC is required. EDMC will now restart." = "A restart of EDMC is required. EDMC will now restart.";
|
||||
|
||||
/* myNotebook.py: Can't Paste Images or Files in Text; */
|
||||
"Cannot paste non-text content." = "Cannot paste non-text content.";
|
||||
|
||||
/* ttkHyperlinkLabel.py: Open Element In Selected Provider; */
|
||||
"Open in {URL}" = "Open in {URL}";
|
||||
|
||||
/* ttkHyperlinkLabel.py: Copy the Inara SLEF Format of the active ship to the clipboard; */
|
||||
"Copy Inara SLEF" = "Copy Inara SLEF";
|
||||
|
19
build.py
19
build.py
@ -10,7 +10,6 @@ import shutil
|
||||
import sys
|
||||
import pathlib
|
||||
from string import Template
|
||||
from os.path import join, isdir
|
||||
import py2exe
|
||||
from config import (
|
||||
appcmdname,
|
||||
@ -37,7 +36,7 @@ def iss_build(template_path: str, output_file: str) -> None:
|
||||
new_file.write(newfile)
|
||||
|
||||
|
||||
def system_check(dist_dir: str) -> str:
|
||||
def system_check(dist_dir: pathlib.Path) -> str:
|
||||
"""Check if the system is able to build."""
|
||||
if sys.version_info < (3, 11):
|
||||
sys.exit(f"Unexpected Python version {sys.version}")
|
||||
@ -55,17 +54,17 @@ def system_check(dist_dir: str) -> str:
|
||||
|
||||
print(f"Git short hash: {git_shorthash}")
|
||||
|
||||
if dist_dir and len(dist_dir) > 1 and isdir(dist_dir):
|
||||
if dist_dir and pathlib.Path.is_dir(dist_dir):
|
||||
shutil.rmtree(dist_dir)
|
||||
return gitversion_file
|
||||
|
||||
|
||||
def generate_data_files(
|
||||
app_name: str, gitversion_file: str, plugins: list[str]
|
||||
) -> list[tuple[str, list[str]]]:
|
||||
) -> list[tuple[object, object]]:
|
||||
"""Create the required datafiles to build."""
|
||||
l10n_dir = "L10n"
|
||||
fdevids_dir = "FDevIDs"
|
||||
fdevids_dir = pathlib.Path("FDevIDs")
|
||||
data_files = [
|
||||
(
|
||||
"",
|
||||
@ -88,13 +87,13 @@ def generate_data_files(
|
||||
),
|
||||
(
|
||||
l10n_dir,
|
||||
[join(l10n_dir, x) for x in os.listdir(l10n_dir) if x.endswith(".strings")],
|
||||
[pathlib.Path(l10n_dir) / x for x in os.listdir(l10n_dir) if x.endswith(".strings")]
|
||||
),
|
||||
(
|
||||
fdevids_dir,
|
||||
[
|
||||
join(fdevids_dir, "commodity.csv"),
|
||||
join(fdevids_dir, "rare_commodity.csv"),
|
||||
pathlib.Path(fdevids_dir / "commodity.csv"),
|
||||
pathlib.Path(fdevids_dir / "rare_commodity.csv"),
|
||||
],
|
||||
),
|
||||
("plugins", plugins),
|
||||
@ -104,7 +103,7 @@ def generate_data_files(
|
||||
|
||||
def build() -> None:
|
||||
"""Build EDMarketConnector using Py2Exe."""
|
||||
dist_dir: str = "dist.win32"
|
||||
dist_dir: pathlib.Path = pathlib.Path("dist.win32")
|
||||
gitversion_filename: str = system_check(dist_dir)
|
||||
|
||||
# Constants
|
||||
@ -142,7 +141,7 @@ def build() -> None:
|
||||
}
|
||||
|
||||
# Function to generate DATA_FILES list
|
||||
data_files: list[tuple[str, list[str]]] = generate_data_files(
|
||||
data_files: list[tuple[object, object]] = generate_data_files(
|
||||
appname, gitversion_filename, plugins
|
||||
)
|
||||
|
||||
|
17
collate.py
17
collate.py
@ -17,7 +17,6 @@ import json
|
||||
import os
|
||||
import pathlib
|
||||
import sys
|
||||
from os.path import isfile
|
||||
from traceback import print_exc
|
||||
|
||||
import companion
|
||||
@ -35,7 +34,7 @@ def __make_backup(file_name: pathlib.Path, suffix: str = '.bak') -> None:
|
||||
"""
|
||||
backup_name = file_name.parent / (file_name.name + suffix)
|
||||
|
||||
if isfile(backup_name):
|
||||
if pathlib.Path.is_file(backup_name):
|
||||
os.unlink(backup_name)
|
||||
|
||||
os.rename(file_name, backup_name)
|
||||
@ -52,13 +51,13 @@ def addcommodities(data) -> None: # noqa: CCR001
|
||||
return
|
||||
|
||||
try:
|
||||
commodityfile = pathlib.Path(config.app_dir_path / 'FDevIDs' / 'commodity.csv')
|
||||
commodityfile = config.app_dir_path / 'FDevIDs' / 'commodity.csv'
|
||||
except FileNotFoundError:
|
||||
commodityfile = pathlib.Path('FDevIDs/commodity.csv')
|
||||
commodities = {}
|
||||
|
||||
# slurp existing
|
||||
if isfile(commodityfile):
|
||||
if pathlib.Path.is_file(commodityfile):
|
||||
with open(commodityfile) as csvfile:
|
||||
reader = csv.DictReader(csvfile)
|
||||
for row in reader:
|
||||
@ -86,7 +85,7 @@ def addcommodities(data) -> None: # noqa: CCR001
|
||||
if len(commodities) <= size_pre:
|
||||
return
|
||||
|
||||
if isfile(commodityfile):
|
||||
if pathlib.Path.is_file(commodityfile):
|
||||
__make_backup(commodityfile)
|
||||
|
||||
with open(commodityfile, 'w', newline='\n') as csvfile:
|
||||
@ -109,7 +108,7 @@ def addmodules(data): # noqa: C901, CCR001
|
||||
fields = ('id', 'symbol', 'category', 'name', 'mount', 'guidance', 'ship', 'class', 'rating', 'entitlement')
|
||||
|
||||
# slurp existing
|
||||
if isfile(outfile):
|
||||
if pathlib.Path.is_file(outfile):
|
||||
with open(outfile) as csvfile:
|
||||
reader = csv.DictReader(csvfile, restval='')
|
||||
for row in reader:
|
||||
@ -147,7 +146,7 @@ def addmodules(data): # noqa: C901, CCR001
|
||||
if not len(modules) > size_pre:
|
||||
return
|
||||
|
||||
if isfile(outfile):
|
||||
if pathlib.Path.is_file(outfile):
|
||||
__make_backup(outfile)
|
||||
|
||||
with open(outfile, 'w', newline='\n') as csvfile:
|
||||
@ -170,7 +169,7 @@ def addships(data) -> None: # noqa: CCR001
|
||||
fields = ('id', 'symbol', 'name')
|
||||
|
||||
# slurp existing
|
||||
if isfile(shipfile):
|
||||
if pathlib.Path.is_file(shipfile):
|
||||
with open(shipfile) as csvfile:
|
||||
reader = csv.DictReader(csvfile, restval='')
|
||||
for row in reader:
|
||||
@ -200,7 +199,7 @@ def addships(data) -> None: # noqa: CCR001
|
||||
if not len(ships) > size_pre:
|
||||
return
|
||||
|
||||
if isfile(shipfile):
|
||||
if pathlib.Path.is_file(shipfile):
|
||||
__make_backup(shipfile)
|
||||
|
||||
with open(shipfile, 'w', newline='\n') as csvfile:
|
||||
|
@ -2,7 +2,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import time
|
||||
from os.path import join
|
||||
from pathlib import Path
|
||||
|
||||
from config import config
|
||||
from edmc_data import commodity_bracketmap as bracketmap
|
||||
@ -29,7 +29,7 @@ def export(data, kind=COMMODITY_DEFAULT, filename=None) -> None:
|
||||
filename_time = time.strftime('%Y-%m-%dT%H.%M.%S', time.localtime(querytime))
|
||||
filename_kind = 'csv'
|
||||
filename = f'{filename_system}.{filename_starport}.{filename_time}.{filename_kind}'
|
||||
filename = join(config.get_str('outdir'), filename)
|
||||
filename = Path(config.get_str('outdir')) / filename
|
||||
|
||||
if kind == COMMODITY_CSV:
|
||||
sep = ';' # BUG: for fixing later after cleanup
|
||||
|
@ -27,6 +27,7 @@ import tkinter as tk
|
||||
import urllib.parse
|
||||
import webbrowser
|
||||
from email.utils import parsedate
|
||||
from pathlib import Path
|
||||
from queue import Queue
|
||||
from typing import TYPE_CHECKING, Any, Mapping, TypeVar
|
||||
import requests
|
||||
@ -1135,7 +1136,7 @@ class Session:
|
||||
|
||||
def dump_capi_data(self, data: CAPIData) -> None:
|
||||
"""Dump CAPI data to file for examination."""
|
||||
if os.path.isdir('dump'):
|
||||
if Path('dump').is_dir():
|
||||
file_name: str = ""
|
||||
if data.source_endpoint == self.FRONTIER_CAPI_PATH_FLEETCARRIER:
|
||||
file_name += f"FleetCarrier.{data['name']['callsign']}"
|
||||
@ -1203,7 +1204,7 @@ def fixup(data: CAPIData) -> CAPIData: # noqa: C901, CCR001 # Can't be usefully
|
||||
if not commodity_map:
|
||||
# Lazily populate
|
||||
for f in ('commodity.csv', 'rare_commodity.csv'):
|
||||
if not os.path.isfile(config.app_dir_path / 'FDevIDs/' / f):
|
||||
if not (config.app_dir_path / 'FDevIDs' / f).is_file():
|
||||
logger.warning(f'FDevID file {f} not found! Generating output without these commodity name rewrites.')
|
||||
continue
|
||||
with open(config.app_dir_path / 'FDevIDs' / f, 'r') as csvfile:
|
||||
|
@ -52,7 +52,7 @@ appcmdname = 'EDMC'
|
||||
# <https://semver.org/#semantic-versioning-specification-semver>
|
||||
# Major.Minor.Patch(-prerelease)(+buildmetadata)
|
||||
# NB: Do *not* import this, use the functions appversion() and appversion_nobuild()
|
||||
_static_appversion = '5.11.1'
|
||||
_static_appversion = '5.11.2'
|
||||
_cached_version: semantic_version.Version | None = None
|
||||
copyright = '© 2015-2019 Jonathan Harris, 2020-2024 EDCD'
|
||||
|
||||
@ -189,6 +189,7 @@ class AbstractConfig(abc.ABC):
|
||||
|
||||
app_dir_path: pathlib.Path
|
||||
plugin_dir_path: pathlib.Path
|
||||
default_plugin_dir_path: pathlib.Path
|
||||
internal_plugin_dir_path: pathlib.Path
|
||||
respath_path: pathlib.Path
|
||||
home_path: pathlib.Path
|
||||
@ -279,6 +280,11 @@ class AbstractConfig(abc.ABC):
|
||||
"""Return a string version of plugin_dir."""
|
||||
return str(self.plugin_dir_path)
|
||||
|
||||
@property
|
||||
def default_plugin_dir(self) -> str:
|
||||
"""Return a string version of plugin_dir."""
|
||||
return str(self.default_plugin_dir_path)
|
||||
|
||||
@property
|
||||
def internal_plugin_dir(self) -> str:
|
||||
"""Return a string version of internal_plugin_dir."""
|
||||
|
@ -31,9 +31,7 @@ class LinuxConfig(AbstractConfig):
|
||||
self.app_dir_path = xdg_data_home / appname
|
||||
self.app_dir_path.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
self.plugin_dir_path = self.app_dir_path / 'plugins'
|
||||
self.plugin_dir_path.mkdir(exist_ok=True)
|
||||
|
||||
self.default_plugin_dir_path = self.app_dir_path / 'plugins'
|
||||
self.respath_path = pathlib.Path(__file__).parent.parent
|
||||
|
||||
self.internal_plugin_dir_path = self.respath_path / 'plugins'
|
||||
@ -62,6 +60,12 @@ class LinuxConfig(AbstractConfig):
|
||||
|
||||
self.config.add_section(self.SECTION)
|
||||
|
||||
if (plugdir_str := self.get_str('plugin_dir')) is None or not pathlib.Path(plugdir_str).is_dir():
|
||||
self.set("plugin_dir", str(self.default_plugin_dir_path))
|
||||
plugdir_str = self.default_plugin_dir
|
||||
self.plugin_dir_path = pathlib.Path(plugdir_str)
|
||||
self.plugin_dir_path.mkdir(exist_ok=True)
|
||||
|
||||
if (outdir := self.get_str('outdir')) is None or not pathlib.Path(outdir).is_dir():
|
||||
self.set('outdir', self.home)
|
||||
|
||||
|
@ -31,25 +31,6 @@ class WinConfig(AbstractConfig):
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
if local_appdata := known_folder_path(shell.FOLDERID_LocalAppData):
|
||||
self.app_dir_path = pathlib.Path(local_appdata) / appname
|
||||
self.app_dir_path.mkdir(exist_ok=True)
|
||||
|
||||
self.plugin_dir_path = self.app_dir_path / 'plugins'
|
||||
self.plugin_dir_path.mkdir(exist_ok=True)
|
||||
|
||||
if getattr(sys, 'frozen', False):
|
||||
self.respath_path = pathlib.Path(sys.executable).parent
|
||||
self.internal_plugin_dir_path = self.respath_path / 'plugins'
|
||||
else:
|
||||
self.respath_path = pathlib.Path(__file__).parent.parent
|
||||
self.internal_plugin_dir_path = self.respath_path / 'plugins'
|
||||
|
||||
self.home_path = pathlib.Path.home()
|
||||
|
||||
journal_dir_path = pathlib.Path(
|
||||
known_folder_path(shell.FOLDERID_SavedGames)) / 'Frontier Developments' / 'Elite Dangerous' # type: ignore
|
||||
self.default_journal_dir_path = journal_dir_path if journal_dir_path.is_dir() else None # type: ignore
|
||||
|
||||
REGISTRY_SUBKEY = r'Software\Marginal\EDMarketConnector' # noqa: N806
|
||||
create_key_defaults = functools.partial(
|
||||
@ -65,6 +46,30 @@ class WinConfig(AbstractConfig):
|
||||
logger.exception('Could not create required registry keys')
|
||||
raise
|
||||
|
||||
if local_appdata := known_folder_path(shell.FOLDERID_LocalAppData):
|
||||
self.app_dir_path = pathlib.Path(local_appdata) / appname
|
||||
self.app_dir_path.mkdir(exist_ok=True)
|
||||
|
||||
self.default_plugin_dir_path = self.app_dir_path / 'plugins'
|
||||
if (plugdir_str := self.get_str('plugin_dir')) is None or not pathlib.Path(plugdir_str).is_dir():
|
||||
self.set("plugin_dir", str(self.default_plugin_dir_path))
|
||||
plugdir_str = self.default_plugin_dir
|
||||
self.plugin_dir_path = pathlib.Path(plugdir_str)
|
||||
self.plugin_dir_path.mkdir(exist_ok=True)
|
||||
|
||||
if getattr(sys, 'frozen', False):
|
||||
self.respath_path = pathlib.Path(sys.executable).parent
|
||||
self.internal_plugin_dir_path = self.respath_path / 'plugins'
|
||||
else:
|
||||
self.respath_path = pathlib.Path(__file__).parent.parent
|
||||
self.internal_plugin_dir_path = self.respath_path / 'plugins'
|
||||
|
||||
self.home_path = pathlib.Path.home()
|
||||
|
||||
journal_dir_path = pathlib.Path(
|
||||
known_folder_path(shell.FOLDERID_SavedGames)) / 'Frontier Developments' / 'Elite Dangerous' # type: ignore
|
||||
self.default_journal_dir_path = journal_dir_path if journal_dir_path.is_dir() else None # type: ignore
|
||||
|
||||
self.identifier = applongname
|
||||
if (outdir_str := self.get_str('outdir')) is None or not pathlib.Path(outdir_str).is_dir():
|
||||
docs = known_folder_path(shell.FOLDERID_Documents)
|
||||
|
@ -51,7 +51,8 @@ if __name__ == "__main__":
|
||||
for m in list(data['Ships'].values()):
|
||||
name = coriolis_ship_map.get(m['properties']['name'], str(m['properties']['name']))
|
||||
assert name in reverse_ship_map, name
|
||||
ships[name] = {'hullMass': m['properties']['hullMass']}
|
||||
ships[name] = {'hullMass': m['properties']['hullMass'],
|
||||
'reserveFuelCapacity': m['properties']['reserveFuelCapacity']}
|
||||
for i, bulkhead in enumerate(bulkheads):
|
||||
modules['_'.join([reverse_ship_map[name], 'armour', bulkhead])] = {'mass': m['bulkheads'][i]['mass']}
|
||||
|
||||
|
@ -12,7 +12,7 @@ import sys
|
||||
import time
|
||||
import tkinter as tk
|
||||
from calendar import timegm
|
||||
from os.path import getsize, isdir, isfile, join
|
||||
from pathlib import Path
|
||||
from typing import Any, cast
|
||||
from watchdog.observers.api import BaseObserver
|
||||
from config import config
|
||||
@ -57,7 +57,7 @@ class Dashboard(FileSystemEventHandler):
|
||||
|
||||
logdir = config.get_str('journaldir', default=config.default_journal_dir)
|
||||
logdir = logdir or config.default_journal_dir
|
||||
if not isdir(logdir):
|
||||
if not Path.is_dir(Path(logdir)):
|
||||
logger.info(f"No logdir, or it isn't a directory: {logdir=}")
|
||||
self.stop()
|
||||
return False
|
||||
@ -164,7 +164,8 @@ class Dashboard(FileSystemEventHandler):
|
||||
|
||||
:param event: Watchdog event.
|
||||
"""
|
||||
if event.is_directory or (isfile(event.src_path) and getsize(event.src_path)):
|
||||
modpath = Path(event.src_path)
|
||||
if event.is_directory or (modpath.is_file() and modpath.stat().st_size):
|
||||
# Can get on_modified events when the file is emptied
|
||||
self.process(event.src_path if not event.is_directory else None)
|
||||
|
||||
@ -177,7 +178,7 @@ class Dashboard(FileSystemEventHandler):
|
||||
if config.shutting_down:
|
||||
return
|
||||
try:
|
||||
status_json_path = join(self.currentdir, 'Status.json')
|
||||
status_json_path = Path(self.currentdir) / 'Status.json'
|
||||
with open(status_json_path, 'rb') as h:
|
||||
data = h.read().strip()
|
||||
if data: # Can be empty if polling while the file is being re-written
|
||||
|
@ -4,21 +4,19 @@ from __future__ import annotations
|
||||
import gzip
|
||||
import json
|
||||
import pathlib
|
||||
import tempfile
|
||||
import threading
|
||||
import zlib
|
||||
from http import server
|
||||
from typing import Any, Callable, Literal
|
||||
from urllib.parse import parse_qs
|
||||
|
||||
from config import appname
|
||||
import config
|
||||
from EDMCLogging import get_main_logger
|
||||
|
||||
logger = get_main_logger()
|
||||
|
||||
output_lock = threading.Lock()
|
||||
output_data_path = pathlib.Path(tempfile.gettempdir()) / f'{appname}' / 'http_debug'
|
||||
SAFE_TRANSLATE = str.maketrans({x: '_' for x in "!@#$%^&*()./\\\r\n[]-+='\";:?<>,~`"})
|
||||
output_data_path = pathlib.Path(config.app_dir_path / 'logs' / 'http_debug')
|
||||
SAFE_TRANSLATE = str.maketrans(dict.fromkeys("!@#$%^&*()./\\\r\n[]-+='\";:?<>,~`", '_'))
|
||||
|
||||
|
||||
class LoggingHandler(server.BaseHTTPRequestHandler):
|
||||
|
@ -3,10 +3,10 @@
|
||||
"""Plugin that tests that modules we bundle for plugins are present and working."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import sqlite3
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
import semantic_version
|
||||
from SubA import SubA
|
||||
@ -14,7 +14,7 @@ from SubA import SubA
|
||||
from config import appname, appversion, config
|
||||
|
||||
# This could also be returned from plugin_start3()
|
||||
plugin_name = os.path.basename(os.path.dirname(__file__))
|
||||
plugin_name = Path(__file__).resolve().parent.name
|
||||
|
||||
# Logger per found plugin, so the folder name is included in
|
||||
# the logging format.
|
||||
@ -49,17 +49,17 @@ class PluginTest:
|
||||
|
||||
def __init__(self, directory: str):
|
||||
logger.debug(f'directory = "{directory}')
|
||||
dbfile = os.path.join(directory, this.DBFILE)
|
||||
dbfile = Path(directory) / this.DBFILE
|
||||
|
||||
# Test 'import zipfile'
|
||||
with zipfile.ZipFile(dbfile + '.zip', 'w') as zip:
|
||||
if os.path.exists(dbfile):
|
||||
with zipfile.ZipFile(str(dbfile) + '.zip', 'w') as zip:
|
||||
if dbfile.exists():
|
||||
zip.write(dbfile)
|
||||
zip.close()
|
||||
|
||||
# Testing 'import shutil'
|
||||
if os.path.exists(dbfile):
|
||||
shutil.copyfile(dbfile, dbfile + '.bak')
|
||||
if dbfile.exists():
|
||||
shutil.copyfile(dbfile, str(dbfile) + '.bak')
|
||||
|
||||
# Testing 'import sqlite3'
|
||||
self.sqlconn = sqlite3.connect(dbfile)
|
||||
|
12
installer.py
12
installer.py
@ -5,23 +5,23 @@ Copyright (c) EDCD, All Rights Reserved
|
||||
Licensed under the GNU General Public License.
|
||||
See LICENSE file.
|
||||
"""
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from build import build
|
||||
|
||||
|
||||
def run_inno_setup_installer(iss_path: str) -> None:
|
||||
def run_inno_setup_installer(iss_path: Path) -> None:
|
||||
"""Run the Inno installer, building the installation exe."""
|
||||
# Get the path to the Inno Setup compiler (iscc.exe) (Currently set to default path)
|
||||
inno_setup_compiler_path: str = "C:\\Program Files (x86)\\Inno Setup 6\\ISCC.exe"
|
||||
inno_setup_compiler_path = Path("C:\\Program Files (x86)\\Inno Setup 6\\ISCC.exe")
|
||||
|
||||
# Check if the Inno Setup compiler executable exists
|
||||
if not os.path.isfile(inno_setup_compiler_path):
|
||||
if not inno_setup_compiler_path.exists():
|
||||
print(f"Error: Inno Setup compiler not found at '{inno_setup_compiler_path}'.")
|
||||
return
|
||||
|
||||
# Check if the provided .iss file exists
|
||||
if not os.path.isfile(iss_file_path):
|
||||
if not iss_file_path.exists():
|
||||
print(f"Error: The provided .iss file '{iss_path}' not found.")
|
||||
return
|
||||
|
||||
@ -40,6 +40,6 @@ def run_inno_setup_installer(iss_path: str) -> None:
|
||||
if __name__ == "__main__":
|
||||
build()
|
||||
# Add the ISS Template File
|
||||
iss_file_path: str = "./EDMC_Installer_Config.iss"
|
||||
iss_file_path = Path("./EDMC_Installer_Config.iss")
|
||||
# Build the ISS file
|
||||
run_inno_setup_installer(iss_file_path)
|
||||
|
44
l10n.py
44
l10n.py
@ -17,10 +17,9 @@ import re
|
||||
import sys
|
||||
import warnings
|
||||
from contextlib import suppress
|
||||
from os import listdir, sep, makedirs
|
||||
from os.path import basename, dirname, isdir, join, abspath, exists
|
||||
from os import listdir, sep
|
||||
from typing import TYPE_CHECKING, Iterable, TextIO, cast
|
||||
|
||||
import pathlib
|
||||
from config import config
|
||||
from EDMCLogging import get_main_logger
|
||||
|
||||
@ -35,7 +34,7 @@ logger = get_main_logger()
|
||||
|
||||
# Language name
|
||||
LANGUAGE_ID = '!Language'
|
||||
LOCALISATION_DIR = 'L10n'
|
||||
LOCALISATION_DIR: pathlib.Path = pathlib.Path('L10n')
|
||||
|
||||
if sys.platform == 'win32':
|
||||
import ctypes
|
||||
@ -118,10 +117,10 @@ class Translations:
|
||||
|
||||
self.translations = {None: self.contents(cast(str, lang))}
|
||||
for plugin in listdir(config.plugin_dir_path):
|
||||
plugin_path = join(config.plugin_dir_path, plugin, LOCALISATION_DIR)
|
||||
if isdir(plugin_path):
|
||||
plugin_path = config.plugin_dir_path / plugin / LOCALISATION_DIR
|
||||
if pathlib.Path.is_dir(plugin_path):
|
||||
try:
|
||||
self.translations[plugin] = self.contents(cast(str, lang), str(plugin_path))
|
||||
self.translations[plugin] = self.contents(cast(str, lang), plugin_path)
|
||||
|
||||
except UnicodeDecodeError as e:
|
||||
logger.warning(f'Malformed file {lang}.strings in plugin {plugin}: {e}')
|
||||
@ -132,7 +131,7 @@ class Translations:
|
||||
# DEPRECATED: Migrate to translations.translate or tr.tl. Will remove in 6.0 or later.
|
||||
builtins.__dict__['_'] = self.translate
|
||||
|
||||
def contents(self, lang: str, plugin_path: str | None = None) -> dict[str, str]:
|
||||
def contents(self, lang: str, plugin_path: pathlib.Path | None = None) -> dict[str, str]:
|
||||
"""Load all the translations from a translation file."""
|
||||
assert lang in self.available()
|
||||
translations = {}
|
||||
@ -172,12 +171,12 @@ class Translations:
|
||||
:return: The translated string
|
||||
"""
|
||||
plugin_name: str | None = None
|
||||
plugin_path: str | None = None
|
||||
plugin_path: pathlib.Path | None = None
|
||||
|
||||
if context:
|
||||
# TODO: There is probably a better way to go about this now.
|
||||
plugin_name = context[len(config.plugin_dir)+1:].split(sep)[0]
|
||||
plugin_path = join(config.plugin_dir_path, plugin_name, LOCALISATION_DIR)
|
||||
plugin_path = config.plugin_dir_path / plugin_name / LOCALISATION_DIR
|
||||
|
||||
if lang:
|
||||
contents: dict[str, str] = self.contents(lang=lang, plugin_path=plugin_path)
|
||||
@ -224,17 +223,17 @@ class Translations:
|
||||
|
||||
return names
|
||||
|
||||
def respath(self) -> str:
|
||||
def respath(self) -> pathlib.Path:
|
||||
"""Path to localisation files."""
|
||||
if getattr(sys, 'frozen', False):
|
||||
return abspath(join(dirname(sys.executable), LOCALISATION_DIR))
|
||||
return pathlib.Path(sys.executable).parent.joinpath(LOCALISATION_DIR).resolve()
|
||||
|
||||
if __file__:
|
||||
return abspath(join(dirname(__file__), LOCALISATION_DIR))
|
||||
return pathlib.Path(__file__).parent.joinpath(LOCALISATION_DIR).resolve()
|
||||
|
||||
return abspath(LOCALISATION_DIR)
|
||||
return LOCALISATION_DIR.resolve()
|
||||
|
||||
def file(self, lang: str, plugin_path: str | None = None) -> TextIO | None:
|
||||
def file(self, lang: str, plugin_path: pathlib.Path | None = None) -> TextIO | None:
|
||||
"""
|
||||
Open the given lang file for reading.
|
||||
|
||||
@ -243,8 +242,8 @@ class Translations:
|
||||
:return: the opened file (Note: This should be closed when done)
|
||||
"""
|
||||
if plugin_path:
|
||||
file_path = join(plugin_path, f'{lang}.strings')
|
||||
if not exists(file_path):
|
||||
file_path = plugin_path / f"{lang}.strings"
|
||||
if not file_path.exists():
|
||||
return None
|
||||
|
||||
try:
|
||||
@ -252,7 +251,7 @@ class Translations:
|
||||
except OSError:
|
||||
logger.exception(f'could not open {file_path}')
|
||||
|
||||
res_path = join(self.respath(), f'{lang}.strings')
|
||||
res_path = self.respath() / f'{lang}.strings'
|
||||
return open(res_path, encoding='utf-8')
|
||||
|
||||
|
||||
@ -381,9 +380,10 @@ Translations: Translations = translations # type: ignore
|
||||
if __name__ == "__main__":
|
||||
regexp = re.compile(r'''_\([ur]?(['"])(((?<!\\)\\\1|.)+?)\1\)[^#]*(#.+)?''') # match a single line python literal
|
||||
seen: dict[str, str] = {}
|
||||
plugin_dir = pathlib.Path('plugins')
|
||||
for f in (
|
||||
sorted(x for x in listdir('.') if x.endswith('.py')) +
|
||||
sorted(join('plugins', x) for x in (listdir('plugins') if isdir('plugins') else []) if x.endswith('.py'))
|
||||
sorted(plugin_dir.glob('*.py')) if plugin_dir.is_dir() else []
|
||||
):
|
||||
with open(f, encoding='utf-8') as h:
|
||||
lineno = 0
|
||||
@ -392,11 +392,11 @@ if __name__ == "__main__":
|
||||
match = regexp.search(line)
|
||||
if match and not seen.get(match.group(2)): # only record first commented instance of a string
|
||||
seen[match.group(2)] = (
|
||||
(match.group(4) and (match.group(4)[1:].strip()) + '. ' or '') + f'[{basename(f)}]'
|
||||
(match.group(4) and (match.group(4)[1:].strip()) + '. ' or '') + f'[{pathlib.Path(f).name}]'
|
||||
)
|
||||
if seen:
|
||||
target_path = join(LOCALISATION_DIR, 'en.template.new')
|
||||
makedirs(dirname(target_path), exist_ok=True)
|
||||
target_path = LOCALISATION_DIR / 'en.template.new'
|
||||
target_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(target_path, 'w', encoding='utf-8') as target_file:
|
||||
target_file.write(f'/* Language name */\n"{LANGUAGE_ID}" = "English";\n\n')
|
||||
for thing in sorted(seen, key=str.lower):
|
||||
|
@ -11,7 +11,7 @@ import json
|
||||
import re
|
||||
import time
|
||||
from os import listdir
|
||||
from os.path import join
|
||||
from pathlib import Path
|
||||
import companion
|
||||
import util_ships
|
||||
from config import config
|
||||
@ -45,7 +45,7 @@ def export(data: companion.CAPIData, requested_filename: str | None = None) -> N
|
||||
regexp = re.compile(re.escape(ship) + r'\.\d\d\d\d-\d\d-\d\dT\d\d\.\d\d\.\d\d\.txt')
|
||||
oldfiles = sorted([x for x in listdir(config.get_str('outdir')) if regexp.match(x)])
|
||||
if oldfiles:
|
||||
with open(join(config.get_str('outdir'), oldfiles[-1]), 'rU') as h:
|
||||
with open(Path(config.get_str('outdir')) / Path(oldfiles[-1]), 'rU') as h:
|
||||
if h.read() == string:
|
||||
return # same as last time - don't write
|
||||
|
||||
@ -53,9 +53,9 @@ def export(data: companion.CAPIData, requested_filename: str | None = None) -> N
|
||||
|
||||
# Write
|
||||
|
||||
output_directory = config.get_str('outdir')
|
||||
output_directory = Path(config.get_str('outdir'))
|
||||
ship_time = time.strftime('%Y-%m-%dT%H.%M.%S', time.localtime(query_time))
|
||||
file_path = join(output_directory, f"{ship}.{ship_time}.txt")
|
||||
file_path = output_directory / f"{ship}.{ship_time}.txt"
|
||||
|
||||
with open(file_path, 'wt') as h:
|
||||
h.write(string)
|
||||
|
70
monitor.py
70
monitor.py
@ -8,7 +8,7 @@ See LICENSE file.
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import pathlib
|
||||
from pathlib import Path
|
||||
import queue
|
||||
import re
|
||||
import sys
|
||||
@ -16,15 +16,15 @@ import threading
|
||||
from calendar import timegm
|
||||
from collections import defaultdict
|
||||
from os import SEEK_END, SEEK_SET, listdir
|
||||
from os.path import basename, expanduser, getctime, isdir, join
|
||||
from time import gmtime, localtime, mktime, sleep, strftime, strptime, time
|
||||
from typing import TYPE_CHECKING, Any, BinaryIO, MutableMapping
|
||||
import psutil
|
||||
import semantic_version
|
||||
import util_ships
|
||||
from config import config
|
||||
from edmc_data import edmc_suit_shortnames, edmc_suit_symbol_localised
|
||||
from config import config, appname, appversion
|
||||
from edmc_data import edmc_suit_shortnames, edmc_suit_symbol_localised, ship_name_map
|
||||
from EDMCLogging import get_main_logger
|
||||
from edshipyard import ships
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import tkinter
|
||||
@ -67,7 +67,7 @@ class EDLogs(FileSystemEventHandler):
|
||||
# TODO(A_D): A bunch of these should be switched to default values (eg '' for strings) and no longer be Optional
|
||||
FileSystemEventHandler.__init__(self) # futureproofing - not need for current version of watchdog
|
||||
self.root: 'tkinter.Tk' = None # type: ignore # Don't use Optional[] - mypy thinks no methods
|
||||
self.currentdir: str | None = None # The actual logdir that we're monitoring
|
||||
self.currentdir: Path | None = None # The actual logdir that we're monitoring
|
||||
self.logfile: str | None = None
|
||||
self.observer: BaseObserver | None = None
|
||||
self.observed = None # a watchdog ObservedWatch, or None if polling
|
||||
@ -96,6 +96,7 @@ class EDLogs(FileSystemEventHandler):
|
||||
self.group: str | None = None
|
||||
self.cmdr: str | None = None
|
||||
self.started: int | None = None # Timestamp of the LoadGame event
|
||||
self.slef: str | None = None
|
||||
|
||||
self._navroute_retries_remaining = 0
|
||||
self._last_navroute_journal_timestamp: float | None = None
|
||||
@ -189,9 +190,9 @@ class EDLogs(FileSystemEventHandler):
|
||||
if journal_dir == '' or journal_dir is None:
|
||||
journal_dir = config.default_journal_dir
|
||||
|
||||
logdir = expanduser(journal_dir)
|
||||
logdir = Path(journal_dir).expanduser()
|
||||
|
||||
if not logdir or not isdir(logdir):
|
||||
if not logdir or not Path.is_dir(logdir):
|
||||
logger.error(f'Journal Directory is invalid: "{logdir}"')
|
||||
self.stop()
|
||||
return False
|
||||
@ -264,9 +265,10 @@ class EDLogs(FileSystemEventHandler):
|
||||
# Odyssey Update 11 has, e.g. Journal.2022-03-15T152503.01.log
|
||||
# Horizons Update 11 equivalent: Journal.220315152335.01.log
|
||||
# So we can no longer use a naive sort.
|
||||
journals_dir_path = pathlib.Path(journals_dir)
|
||||
journal_files = (journals_dir_path / pathlib.Path(x) for x in journal_files)
|
||||
return str(max(journal_files, key=getctime))
|
||||
journals_dir_path = Path(journals_dir)
|
||||
journal_files = (journals_dir_path / Path(x) for x in journal_files)
|
||||
latest_file = max(journal_files, key=lambda f: Path(f).stat().st_ctime)
|
||||
return str(latest_file)
|
||||
|
||||
return None
|
||||
|
||||
@ -335,7 +337,7 @@ class EDLogs(FileSystemEventHandler):
|
||||
|
||||
def on_created(self, event: 'FileSystemEvent') -> None:
|
||||
"""Watchdog callback when, e.g. client (re)started."""
|
||||
if not event.is_directory and self._RE_LOGFILE.search(basename(event.src_path)):
|
||||
if not event.is_directory and self._RE_LOGFILE.search(Path(event.src_path).name):
|
||||
|
||||
self.logfile = event.src_path
|
||||
|
||||
@ -691,6 +693,34 @@ class EDLogs(FileSystemEventHandler):
|
||||
module.pop('AmmoInHopper')
|
||||
|
||||
self.state['Modules'][module['Slot']] = module
|
||||
# SLEF
|
||||
initial_dict: dict[str, dict[str, Any]] = {
|
||||
"header": {"appName": appname, "appVersion": str(appversion())}
|
||||
}
|
||||
data_dict = {}
|
||||
for module in entry['Modules']:
|
||||
if module.get('Slot') == 'FuelTank':
|
||||
cap = module['Item'].split('size')
|
||||
cap = cap[1].split('_')
|
||||
cap = 2 ** int(cap[0])
|
||||
ship = ship_name_map[entry["Ship"]]
|
||||
fuel = {'Main': cap, 'Reserve': ships[ship]['reserveFuelCapacity']}
|
||||
data_dict.update({"FuelCapacity": fuel})
|
||||
data_dict.update({
|
||||
'Ship': entry["Ship"],
|
||||
'ShipName': entry['ShipName'],
|
||||
'ShipIdent': entry['ShipIdent'],
|
||||
'HullValue': entry['HullValue'],
|
||||
'ModulesValue': entry['ModulesValue'],
|
||||
'Rebuy': entry['Rebuy'],
|
||||
'MaxJumpRange': entry['MaxJumpRange'],
|
||||
'UnladenMass': entry['UnladenMass'],
|
||||
'CargoCapacity': entry['CargoCapacity'],
|
||||
'Modules': entry['Modules'],
|
||||
})
|
||||
initial_dict.update({'data': data_dict})
|
||||
output = json.dumps(initial_dict, indent=4)
|
||||
self.slef = str(f"[{output}]")
|
||||
|
||||
elif event_type == 'modulebuy':
|
||||
self.state['Modules'][entry['Slot']] = {
|
||||
@ -1046,7 +1076,7 @@ class EDLogs(FileSystemEventHandler):
|
||||
self.state['Cargo'] = defaultdict(int)
|
||||
# From 3.3 full Cargo event (after the first one) is written to a separate file
|
||||
if 'Inventory' not in entry:
|
||||
with open(join(self.currentdir, 'Cargo.json'), 'rb') as h: # type: ignore
|
||||
with open(self.currentdir / 'Cargo.json', 'rb') as h: # type: ignore
|
||||
entry = json.load(h)
|
||||
self.state['CargoJSON'] = entry
|
||||
|
||||
@ -1073,7 +1103,7 @@ class EDLogs(FileSystemEventHandler):
|
||||
# Always attempt loading of this, but if it fails we'll hope this was
|
||||
# a startup/boarding version and thus `entry` contains
|
||||
# the data anyway.
|
||||
currentdir_path = pathlib.Path(str(self.currentdir))
|
||||
currentdir_path = Path(str(self.currentdir))
|
||||
shiplocker_filename = currentdir_path / 'ShipLocker.json'
|
||||
shiplocker_max_attempts = 5
|
||||
shiplocker_fail_sleep = 0.01
|
||||
@ -1142,7 +1172,7 @@ class EDLogs(FileSystemEventHandler):
|
||||
|
||||
# TODO: v31 doc says this is`backpack.json` ... but Howard Chalkley
|
||||
# said it's `Backpack.json`
|
||||
backpack_file = pathlib.Path(str(self.currentdir)) / 'Backpack.json'
|
||||
backpack_file = Path(str(self.currentdir)) / 'Backpack.json'
|
||||
backpack_data = None
|
||||
|
||||
if not backpack_file.exists():
|
||||
@ -1518,7 +1548,7 @@ class EDLogs(FileSystemEventHandler):
|
||||
entry = fcmaterials
|
||||
|
||||
elif event_type == 'moduleinfo':
|
||||
with open(join(self.currentdir, 'ModulesInfo.json'), 'rb') as mf: # type: ignore
|
||||
with open(self.currentdir / 'ModulesInfo.json', 'rb') as mf: # type: ignore
|
||||
try:
|
||||
entry = json.load(mf)
|
||||
|
||||
@ -2230,14 +2260,14 @@ class EDLogs(FileSystemEventHandler):
|
||||
oldfiles = sorted((x for x in listdir(config.get_str('outdir')) if regexp.match(x)))
|
||||
if oldfiles:
|
||||
try:
|
||||
with open(join(config.get_str('outdir'), oldfiles[-1]), encoding='utf-8') as h:
|
||||
with open(config.get_str('outdir') / Path(oldfiles[-1]), encoding='utf-8') as h:
|
||||
if h.read() == string:
|
||||
return # same as last time - don't write
|
||||
|
||||
except UnicodeError:
|
||||
logger.exception("UnicodeError reading old ship loadout with utf-8 encoding, trying without...")
|
||||
try:
|
||||
with open(join(config.get_str('outdir'), oldfiles[-1])) as h:
|
||||
with open(config.get_str('outdir') / Path(oldfiles[-1])) as h:
|
||||
if h.read() == string:
|
||||
return # same as last time - don't write
|
||||
|
||||
@ -2256,7 +2286,7 @@ class EDLogs(FileSystemEventHandler):
|
||||
|
||||
# Write
|
||||
ts = strftime('%Y-%m-%dT%H.%M.%S', localtime(time()))
|
||||
filename = join(config.get_str('outdir'), f'{ship}.{ts}.txt')
|
||||
filename = config.get_str('outdir') / Path(f'{ship}.{ts}.txt')
|
||||
|
||||
try:
|
||||
with open(filename, 'wt', encoding='utf-8') as h:
|
||||
@ -2343,7 +2373,7 @@ class EDLogs(FileSystemEventHandler):
|
||||
|
||||
try:
|
||||
|
||||
with open(join(self.currentdir, 'NavRoute.json')) as f:
|
||||
with open(self.currentdir / 'NavRoute.json') as f:
|
||||
raw = f.read()
|
||||
|
||||
except Exception as e:
|
||||
@ -2369,7 +2399,7 @@ class EDLogs(FileSystemEventHandler):
|
||||
|
||||
try:
|
||||
|
||||
with open(join(self.currentdir, 'FCMaterials.json')) as f:
|
||||
with open(self.currentdir / 'FCMaterials.json') as f:
|
||||
raw = f.read()
|
||||
|
||||
except Exception as e:
|
||||
|
22
plug.py
22
plug.py
@ -14,6 +14,7 @@ import operator
|
||||
import os
|
||||
import sys
|
||||
import tkinter as tk
|
||||
from pathlib import Path
|
||||
from tkinter import ttk
|
||||
from typing import Any, Mapping, MutableMapping
|
||||
|
||||
@ -47,7 +48,7 @@ last_error = LastError()
|
||||
class Plugin:
|
||||
"""An EDMC plugin."""
|
||||
|
||||
def __init__(self, name: str, loadfile: str | None, plugin_logger: logging.Logger | None): # noqa: CCR001
|
||||
def __init__(self, name: str, loadfile: Path | None, plugin_logger: logging.Logger | None): # noqa: CCR001
|
||||
"""
|
||||
Load a single plugin.
|
||||
|
||||
@ -73,7 +74,7 @@ class Plugin:
|
||||
sys.modules[module.__name__] = module
|
||||
spec.loader.exec_module(module)
|
||||
if getattr(module, 'plugin_start3', None):
|
||||
newname = module.plugin_start3(os.path.dirname(loadfile))
|
||||
newname = module.plugin_start3(Path(loadfile).resolve().parent)
|
||||
self.name = str(newname) if newname else self.name
|
||||
self.module = module
|
||||
elif getattr(module, 'plugin_start', None):
|
||||
@ -171,7 +172,9 @@ def _load_internal_plugins():
|
||||
for name in sorted(os.listdir(config.internal_plugin_dir_path)):
|
||||
if name.endswith('.py') and name[0] not in ('.', '_'):
|
||||
try:
|
||||
plugin = Plugin(name[:-3], os.path.join(config.internal_plugin_dir_path, name), logger)
|
||||
plugin_name = name[:-3]
|
||||
plugin_path = config.internal_plugin_dir_path / name
|
||||
plugin = Plugin(plugin_name, plugin_path, logger)
|
||||
plugin.folder = None
|
||||
internal.append(plugin)
|
||||
except Exception:
|
||||
@ -186,9 +189,12 @@ def _load_found_plugins():
|
||||
# The intent here is to e.g. have EDMC-Overlay load before any plugins
|
||||
# that depend on it.
|
||||
|
||||
for name in sorted(os.listdir(config.plugin_dir_path), key=lambda n: (
|
||||
not os.path.isfile(os.path.join(config.plugin_dir_path, n, '__init__.py')), n.lower())):
|
||||
if not os.path.isdir(os.path.join(config.plugin_dir_path, name)) or name[0] in ('.', '_'):
|
||||
plugin_files = sorted(config.plugin_dir_path.iterdir(), key=lambda p: (
|
||||
not (p / '__init__.py').is_file(), p.name.lower()))
|
||||
|
||||
for plugin_file in plugin_files:
|
||||
name = plugin_file.name
|
||||
if not (config.plugin_dir_path / name).is_dir() or name.startswith(('.', '_')):
|
||||
pass
|
||||
elif name.endswith('.disabled'):
|
||||
name, discard = name.rsplit('.', 1)
|
||||
@ -196,12 +202,12 @@ def _load_found_plugins():
|
||||
else:
|
||||
try:
|
||||
# Add plugin's folder to load path in case plugin has internal package dependencies
|
||||
sys.path.append(os.path.join(config.plugin_dir_path, name))
|
||||
sys.path.append(str(config.plugin_dir_path / name))
|
||||
|
||||
import EDMCLogging
|
||||
# Create a logger for this 'found' plugin. Must be before the load.py is loaded.
|
||||
plugin_logger = EDMCLogging.get_plugin_logger(name)
|
||||
found.append(Plugin(name, os.path.join(config.plugin_dir_path, name, 'load.py'), plugin_logger))
|
||||
found.append(Plugin(name, config.plugin_dir_path / name / 'load.py', plugin_logger))
|
||||
except Exception:
|
||||
PLUGINS_broken.append(Plugin(name, None, logger))
|
||||
logger.exception(f'Failure loading found Plugin "{name}"')
|
||||
|
107
prefs.py
107
prefs.py
@ -4,14 +4,13 @@ from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import logging
|
||||
import pathlib
|
||||
from os.path import expandvars
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import tkinter as tk
|
||||
import warnings
|
||||
from os import system
|
||||
from os.path import expanduser, expandvars, join, normpath
|
||||
from tkinter import colorchooser as tkColorChooser # type: ignore # noqa: N812
|
||||
from tkinter import ttk
|
||||
from types import TracebackType
|
||||
@ -20,7 +19,6 @@ import myNotebook as nb # noqa: N813
|
||||
import plug
|
||||
from config import appversion_nobuild, config
|
||||
from EDMCLogging import edmclogger, get_main_logger
|
||||
from constants import appname
|
||||
from hotkey import hotkeymgr
|
||||
from l10n import translations as tr
|
||||
from monitor import monitor
|
||||
@ -43,10 +41,10 @@ def help_open_log_folder() -> None:
|
||||
"""Open the folder logs are stored in."""
|
||||
warnings.warn('prefs.help_open_log_folder is deprecated, use open_log_folder instead. '
|
||||
'This function will be removed in 6.0 or later', DeprecationWarning, stacklevel=2)
|
||||
open_folder(pathlib.Path(tempfile.gettempdir()) / appname)
|
||||
open_folder(Path(config.app_dir_path / 'logs'))
|
||||
|
||||
|
||||
def open_folder(file: pathlib.Path) -> None:
|
||||
def open_folder(file: Path) -> None:
|
||||
"""Open the given file in the OS file explorer."""
|
||||
if sys.platform.startswith('win'):
|
||||
# On Windows, use the "start" command to open the folder
|
||||
@ -58,7 +56,7 @@ def open_folder(file: pathlib.Path) -> None:
|
||||
|
||||
def help_open_system_profiler(parent) -> None:
|
||||
"""Open the EDMC System Profiler."""
|
||||
profiler_path = pathlib.Path(config.respath_path)
|
||||
profiler_path = config.respath_path
|
||||
try:
|
||||
if getattr(sys, 'frozen', False):
|
||||
profiler_path /= 'EDMCSystemProfiler.exe'
|
||||
@ -235,6 +233,7 @@ class PreferencesDialog(tk.Toplevel):
|
||||
|
||||
self.parent = parent
|
||||
self.callback = callback
|
||||
self.req_restart = False
|
||||
# LANG: File > Settings (macOS)
|
||||
self.title(tr.tl('Settings'))
|
||||
|
||||
@ -319,7 +318,7 @@ class PreferencesDialog(tk.Toplevel):
|
||||
self.geometry(f"+{position.left}+{position.top}")
|
||||
|
||||
# Set Log Directory
|
||||
self.logfile_loc = pathlib.Path(tempfile.gettempdir()) / appname
|
||||
self.logfile_loc = Path(config.app_dir_path / 'logs')
|
||||
|
||||
# Set minimum size to prevent content cut-off
|
||||
self.update_idletasks() # Update "requested size" from geometry manager
|
||||
@ -907,20 +906,15 @@ class PreferencesDialog(tk.Toplevel):
|
||||
# Plugin settings and info
|
||||
plugins_frame = nb.Frame(notebook)
|
||||
plugins_frame.columnconfigure(0, weight=1)
|
||||
plugdir = tk.StringVar()
|
||||
plugdir.set(config.plugin_dir)
|
||||
row = AutoInc(start=0)
|
||||
|
||||
# Section heading in settings
|
||||
self.plugdir = tk.StringVar()
|
||||
self.plugdir.set(str(config.get_str('plugin_dir')))
|
||||
# LANG: Label for location of third-party plugins folder
|
||||
nb.Label(plugins_frame, text=tr.tl('Plugins folder') + ':').grid(
|
||||
padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get()
|
||||
)
|
||||
|
||||
plugdirentry = ttk.Entry(plugins_frame, justify=tk.LEFT)
|
||||
self.displaypath(plugdir, plugdirentry)
|
||||
plugdirentry.grid(columnspan=2, padx=self.PADX, pady=self.BOXY, sticky=tk.EW, row=row.get())
|
||||
|
||||
self.plugdir_label = nb.Label(plugins_frame, text=tr.tl('Plugins folder') + ':')
|
||||
self.plugdir_label.grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get())
|
||||
self.plugdir_entry = ttk.Entry(plugins_frame, takefocus=False,
|
||||
textvariable=self.plugdir) # Link StringVar to Entry widget
|
||||
self.plugdir_entry.grid(columnspan=4, padx=self.PADX, pady=self.BOXY, sticky=tk.EW, row=row.get())
|
||||
with row as cur_row:
|
||||
nb.Label(
|
||||
plugins_frame,
|
||||
@ -928,19 +922,41 @@ class PreferencesDialog(tk.Toplevel):
|
||||
# LANG: Tip/label about how to disable plugins
|
||||
text=tr.tl(
|
||||
"Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name").format(EXT='.disabled')
|
||||
).grid(columnspan=2, padx=self.PADX, pady=self.PADY, sticky=tk.EW, row=cur_row)
|
||||
).grid(columnspan=1, padx=self.PADX, pady=self.PADY, sticky=tk.EW, row=cur_row)
|
||||
|
||||
ttk.Button(
|
||||
# Open Plugin Folder Button
|
||||
self.open_plug_folder_btn = ttk.Button(
|
||||
plugins_frame,
|
||||
# LANG: Label on button used to open a filesystem folder
|
||||
text=tr.tl('Open'), # Button that opens a folder in Explorer/Finder
|
||||
# LANG: Label on button used to open the Plugin Folder
|
||||
text=tr.tl('Open Plugins Folder'),
|
||||
command=lambda: open_folder(config.plugin_dir_path)
|
||||
).grid(column=1, padx=self.PADX, pady=self.PADY, sticky=tk.N, row=cur_row)
|
||||
)
|
||||
self.open_plug_folder_btn.grid(column=1, padx=self.PADX, pady=self.PADY, sticky=tk.EW, row=cur_row)
|
||||
|
||||
# Browse Button
|
||||
text = tr.tl('Browse...') # LANG: NOT-macOS Settings - files location selection button
|
||||
self.plugbutton = ttk.Button(
|
||||
plugins_frame,
|
||||
text=text,
|
||||
# LANG: Selecting the Location of the Plugin Directory on the Filesystem
|
||||
command=lambda: self.filebrowse(tr.tl('Plugin Directory Location'), self.plugdir)
|
||||
)
|
||||
self.plugbutton.grid(column=2, padx=self.PADX, pady=self.PADY, sticky=tk.EW, row=cur_row)
|
||||
|
||||
if config.default_journal_dir_path:
|
||||
# Appearance theme and language setting
|
||||
ttk.Button(
|
||||
plugins_frame,
|
||||
# LANG: Settings > Configuration - Label on 'reset journal files location to default' button
|
||||
text=tr.tl('Default'),
|
||||
command=self.plugdir_reset,
|
||||
state=tk.NORMAL if config.get_str('plugin_dir') else tk.DISABLED
|
||||
).grid(column=3, padx=self.PADX, pady=self.PADY, sticky=tk.EW, row=cur_row)
|
||||
|
||||
enabled_plugins = list(filter(lambda x: x.folder and x.module, plug.PLUGINS))
|
||||
if enabled_plugins:
|
||||
ttk.Separator(plugins_frame, orient=tk.HORIZONTAL).grid(
|
||||
columnspan=3, padx=self.PADX, pady=self.SEPY, sticky=tk.EW, row=row.get()
|
||||
columnspan=4, padx=self.PADX, pady=self.SEPY, sticky=tk.EW, row=row.get()
|
||||
)
|
||||
nb.Label(
|
||||
plugins_frame,
|
||||
@ -1062,7 +1078,7 @@ class PreferencesDialog(tk.Toplevel):
|
||||
import tkinter.filedialog
|
||||
directory = tkinter.filedialog.askdirectory(
|
||||
parent=self,
|
||||
initialdir=expanduser(pathvar.get()),
|
||||
initialdir=Path(pathvar.get()).expanduser(),
|
||||
title=title,
|
||||
mustexist=tk.TRUE
|
||||
)
|
||||
@ -1084,7 +1100,7 @@ class PreferencesDialog(tk.Toplevel):
|
||||
if sys.platform == 'win32':
|
||||
start = len(config.home.split('\\')) if pathvar.get().lower().startswith(config.home.lower()) else 0
|
||||
display = []
|
||||
components = normpath(pathvar.get()).split('\\')
|
||||
components = Path(pathvar.get()).resolve().parts
|
||||
buf = ctypes.create_unicode_buffer(MAX_PATH)
|
||||
pidsRes = ctypes.c_int() # noqa: N806 # Windows convention
|
||||
for i in range(start, len(components)):
|
||||
@ -1119,6 +1135,13 @@ class PreferencesDialog(tk.Toplevel):
|
||||
|
||||
self.outvarchanged()
|
||||
|
||||
def plugdir_reset(self) -> None:
|
||||
"""Reset the log dir to the default."""
|
||||
if config.default_plugin_dir_path:
|
||||
self.plugdir.set(config.default_plugin_dir)
|
||||
|
||||
self.outvarchanged()
|
||||
|
||||
def disable_autoappupdatecheckingame_changed(self) -> None:
|
||||
"""Save out the auto update check in game config."""
|
||||
config.set('disable_autoappupdatecheckingame', self.disable_autoappupdatecheckingame.get())
|
||||
@ -1214,7 +1237,7 @@ class PreferencesDialog(tk.Toplevel):
|
||||
|
||||
return 'break' # stops further processing - insertion, Tab traversal etc
|
||||
|
||||
def apply(self) -> None:
|
||||
def apply(self) -> None: # noqa: CCR001
|
||||
"""Update the config with the options set on the dialog."""
|
||||
config.set('PrefsVersion', prefsVersion.stringToSerial(appversion_nobuild()))
|
||||
config.set(
|
||||
@ -1230,7 +1253,7 @@ class PreferencesDialog(tk.Toplevel):
|
||||
|
||||
config.set(
|
||||
'outdir',
|
||||
join(config.home_path, self.outdir.get()[2:]) if self.outdir.get().startswith('~') else self.outdir.get()
|
||||
str(config.home_path / self.outdir.get()[2:]) if self.outdir.get().startswith('~') else self.outdir.get()
|
||||
)
|
||||
|
||||
logdir = self.logdir.get()
|
||||
@ -1270,20 +1293,30 @@ class PreferencesDialog(tk.Toplevel):
|
||||
config.set('dark_text', self.theme_colors[0])
|
||||
config.set('dark_highlight', self.theme_colors[1])
|
||||
theme.apply(self.parent)
|
||||
if self.plugdir.get() != config.get('plugin_dir'):
|
||||
config.set(
|
||||
'plugin_dir',
|
||||
str(Path(config.home_path, self.plugdir.get()[2:])) if self.plugdir.get().startswith('~') else
|
||||
str(Path(self.plugdir.get()))
|
||||
)
|
||||
self.req_restart = True
|
||||
|
||||
# Send to the Post Config if we updated the update branch
|
||||
post_flags = {
|
||||
'Update': True if self.curr_update_track != self.update_paths.get() else False,
|
||||
'Track': self.update_paths.get(),
|
||||
'Parent': self
|
||||
}
|
||||
# Notify
|
||||
if self.callback:
|
||||
self.callback(**post_flags)
|
||||
self.callback()
|
||||
|
||||
plug.notify_prefs_changed(monitor.cmdr, monitor.is_beta)
|
||||
|
||||
self._destroy()
|
||||
# Send to the Post Config if we updated the update branch or need to restart
|
||||
post_flags = {
|
||||
'Update': True if self.curr_update_track != self.update_paths.get() else False,
|
||||
'Track': self.update_paths.get(),
|
||||
'Parent': self,
|
||||
'Restart_Req': True if self.req_restart else False
|
||||
}
|
||||
if self.callback:
|
||||
self.callback(**post_flags)
|
||||
|
||||
def _destroy(self) -> None:
|
||||
"""widget.destroy wrapper that does some extra cleanup."""
|
||||
|
@ -8,10 +8,10 @@ wheel
|
||||
setuptools==70.0.0
|
||||
|
||||
# Static analysis tools
|
||||
flake8==7.0.0
|
||||
flake8==7.1.0
|
||||
flake8-annotations-coverage==0.0.6
|
||||
flake8-cognitive-complexity==0.1.0
|
||||
flake8-comprehensions==3.14.0
|
||||
flake8-comprehensions==3.15.0
|
||||
flake8-docstrings==1.7.0
|
||||
flake8-json==24.4.0
|
||||
flake8-noqa==1.4.0
|
||||
@ -19,7 +19,7 @@ flake8-polyfill==1.0.2
|
||||
flake8-use-fstring==1.4
|
||||
|
||||
mypy==1.10.0
|
||||
pep8-naming==0.13.3
|
||||
pep8-naming==0.14.1
|
||||
safety==3.2.0
|
||||
types-requests==2.31.0.20240311
|
||||
types-pkg-resources==0.1.3
|
||||
@ -31,14 +31,14 @@ autopep8==2.2.0
|
||||
pre-commit==3.7.1
|
||||
|
||||
# HTML changelogs
|
||||
grip==4.6.2
|
||||
mistune==3.0.2
|
||||
|
||||
# Packaging
|
||||
# We only need py2exe on windows.
|
||||
py2exe==0.13.0.1; sys_platform == 'win32'
|
||||
|
||||
# Testing
|
||||
pytest==8.2.0
|
||||
pytest==8.2.2
|
||||
pytest-cov==5.0.0 # Pytest code coverage support
|
||||
coverage[toml]==7.5.0 # pytest-cov dep. This is here to ensure that it includes TOML support for pyproject.toml configs
|
||||
coverage-conditional-plugin==0.9.0
|
||||
|
@ -1,6 +1,6 @@
|
||||
requests==2.32.3
|
||||
pillow==10.3.0
|
||||
watchdog==4.0.0
|
||||
watchdog==4.0.1
|
||||
simplesystray==0.1.0; sys_platform == 'win32'
|
||||
semantic-version==2.10.0
|
||||
# For manipulating folder permissions and the like.
|
||||
|
165
scripts/build_changelog.py
Normal file
165
scripts/build_changelog.py
Normal file
@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env python3
|
||||
# flake8: noqa
|
||||
"""
|
||||
build_changelog.py - Read the latest changelog file and format for the Forums and Reddit.
|
||||
|
||||
Copyright (c) EDCD, All Rights Reserved
|
||||
Licensed under the GNU General Public License.
|
||||
See LICENSE file.
|
||||
"""
|
||||
|
||||
import pathlib
|
||||
import re
|
||||
from os import chdir
|
||||
import mistune
|
||||
|
||||
|
||||
def get_changelog() -> tuple[str, str]:
|
||||
"""Pull the last full changelog details in MD."""
|
||||
try:
|
||||
with open("../CHANGELOG.md", encoding="utf-8") as changelog_file:
|
||||
content = changelog_file.read()
|
||||
except FileNotFoundError as exc:
|
||||
raise FileNotFoundError("Changelog file not found.") from exc
|
||||
|
||||
changelog_list = content.split("---", maxsplit=2)
|
||||
if len(changelog_list) < 3:
|
||||
raise ValueError("Changelog format is incorrect.")
|
||||
|
||||
changelog = changelog_list[2].split("===", maxsplit=2)
|
||||
if len(changelog) < 2:
|
||||
raise ValueError("Changelog format is incorrect.")
|
||||
|
||||
changelog[0] = changelog[0].strip()
|
||||
changelog[1] = "\n".join(changelog[1].strip().split("\n")[:-2])
|
||||
version = changelog[0]
|
||||
version = version.split(" ")[1]
|
||||
final_changelog = changelog[1].strip()
|
||||
|
||||
return final_changelog, version
|
||||
|
||||
|
||||
def build_html(md_changelog: str, version: str) -> str:
|
||||
"""Convert markdown changelog to HTML."""
|
||||
html_out = f"<h2>Release {version}</h2>\n"
|
||||
html_out += mistune.html(md_changelog)
|
||||
html_out = re.sub(r"h1", "h2", html_out) + "\n<hr>"
|
||||
|
||||
with open("script_output/html_changelog.txt", "w", encoding="utf-8") as html_file:
|
||||
html_file.write(html_out)
|
||||
|
||||
return html_out
|
||||
|
||||
|
||||
def format_fdev(md_log: str) -> str:
|
||||
"""Format changelog for FDEV forums."""
|
||||
md_log = re.sub(r"<p>|</p>", "", md_log)
|
||||
md_log = re.sub(r"<strong>", "\n[HEADING=3]", md_log)
|
||||
md_log = re.sub(r"</strong>", "[/HEADING]", md_log)
|
||||
md_log = re.sub(r"<ul>", "[LIST]", md_log)
|
||||
md_log = re.sub(r"<li>", "[*]", md_log)
|
||||
md_log = re.sub(r"</li>", "", md_log)
|
||||
md_log = re.sub(r"<code>", "[ICODE]", md_log)
|
||||
md_log = re.sub(r"</code>", "[/ICODE]", md_log)
|
||||
md_log = re.sub(r"</ul>\n", "[/LIST]", md_log)
|
||||
md_log = re.sub(r"<hr>", "", md_log)
|
||||
md_log = re.sub(r"Changes and Enhancements", "What's Changed", md_log)
|
||||
return md_log
|
||||
|
||||
|
||||
def build_fdev(
|
||||
vt_signed: str, vt_unsigned: str, version: str, gh_link: str, html: str
|
||||
) -> None:
|
||||
"""Build changelog for FDEV forums."""
|
||||
fdev_out = (
|
||||
f"[HEADING=2][URL='{gh_link}'][SIZE=7]Release {version}[/SIZE][/URL][/HEADING]\n"
|
||||
f"[URL='{vt_signed}']Pre-emptive upload to VirusTotal[/URL]. "
|
||||
f"([URL='{vt_unsigned}']Unsigned Installer[/URL])\n\n"
|
||||
)
|
||||
|
||||
if version.startswith(("Pre-Release", "Beta")):
|
||||
fdev_out += (
|
||||
f'This is a release candidate for {version}. It has been pushed to the "Beta" track for updates!\n\n'
|
||||
'For more information on the "Beta" update track, please read '
|
||||
"[URL='https://github.com/EDCD/EDMarketConnector/wiki/Participating-in-Open-Betas-of-EDMC']"
|
||||
"This Wiki Article[/URL]. Questions and comments are welcome!\n\n"
|
||||
)
|
||||
|
||||
md_log = html.split("\n", maxsplit=1)[1]
|
||||
md_log = format_fdev(md_log)
|
||||
fdev_out += md_log
|
||||
|
||||
with open("script_output/fdev_changelog.txt", "w", encoding="utf-8") as fdev_file:
|
||||
fdev_file.write(fdev_out)
|
||||
|
||||
|
||||
def build_reddit(
|
||||
md_changelog: str, vt_signed: str, vt_unsigned: str, version: str, gh_link: str
|
||||
) -> None:
|
||||
"""Build changelog for Reddit."""
|
||||
reddit_start = """# What Is Elite Dangerous Market Connector?
|
||||
|
||||
Elite Dangerous Market Connector ("EDMC") is a third-party application for use with Frontier Developments' game "Elite Dangerous". Its purpose is to facilitate supplying certain game data to, and in some cases retrieving it from, a number of websites and other tools.
|
||||
|
||||
To achieve this it utilizes the Journal Files written by the game when played on a PC. It also makes use of Frontier's Companion API ("Frontier's CAPI"), accessible once you've authorized this application.
|
||||
|
||||
EDMC has a plugin system that many other developers have made use of to extend its functionality.
|
||||
|
||||
Find out more on the [EDMC Wiki](https://github.com/EDCD/EDMarketConnector/wiki).
|
||||
|
||||
~~----------------------------------------------------~~
|
||||
|
||||
You can also view the Elite: Dangerous Forum thread [HERE](https://forums.frontier.co.uk/threads/elite-dangerous-market-connector-edmc.618708/).
|
||||
|
||||
~~----------------------------------------------------~~
|
||||
|
||||
**As has become routine now, various anti-virus software are reporting a false positive on our installer and/or files it contains. We've pre-emptively uploaded the installer to** [VirusTotal]("""
|
||||
reddit_mid_1 = """) **if you want to check what it's saying. Please see our** [Troubleshooting/AV-false-positives FAQ](https://github.com/EDCD/EDMarketConnector/wiki/Troubleshooting#installer-and-or-executables-flagged-as-malicious-viruses) **for further information.**
|
||||
|
||||
[Unsigned Installer]("""
|
||||
|
||||
reddit_mid_2 = """) if you don't want to use the code-signed option.
|
||||
|
||||
~~----------------------------------------------------~~
|
||||
"""
|
||||
updated = f"# [Release {version}]({gh_link})\n\n"
|
||||
md_changelog = re.sub(r"===\n", "", md_changelog)
|
||||
md_changelog = re.sub(f"Release {version}", updated, md_changelog)
|
||||
reddit_end = f"""
|
||||
|
||||
**Linux**
|
||||
|
||||
If you're running on Linux, try the [Flatpak](https://flathub.org/apps/io.edcd.EDMarketConnector) build of EDMC! (Update to {version} coming soon.)"""
|
||||
|
||||
reddit_out = (
|
||||
reddit_start
|
||||
+ vt_signed
|
||||
+ reddit_mid_1
|
||||
+ vt_unsigned
|
||||
+ reddit_mid_2
|
||||
+ updated
|
||||
+ md_changelog
|
||||
+ reddit_end
|
||||
)
|
||||
|
||||
with open(
|
||||
"script_output/reddit_changelog.txt", "w", encoding="utf-8"
|
||||
) as reddit_file:
|
||||
reddit_file.write(reddit_out)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Run the Changelog Generator"""
|
||||
md_changelog, version = get_changelog()
|
||||
print(f"Detected version {version} in the changelog. Continuing...")
|
||||
gh_link = input(f"Please enter the GitHub link for {version}: ")
|
||||
vt_signed = input("Please enter the VirusTotal URL for the Signed Installer: ")
|
||||
vt_unsigned = input("Please enter the VirusTotal URL for the Unsigned Installer: ")
|
||||
build_reddit(md_changelog, vt_signed, vt_unsigned, version, gh_link)
|
||||
html = build_html(md_changelog, version)
|
||||
build_fdev(vt_signed, vt_unsigned, version, gh_link, html)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
chdir(pathlib.Path(__file__).parent)
|
||||
main()
|
117
ships.json
117
ships.json
@ -1,119 +1,158 @@
|
||||
{
|
||||
"Adder": {
|
||||
"hullMass": 35
|
||||
"hullMass": 35,
|
||||
"reserveFuelCapacity": 0.36
|
||||
},
|
||||
"Alliance Challenger": {
|
||||
"hullMass": 450
|
||||
"hullMass": 450,
|
||||
"reserveFuelCapacity": 0.77
|
||||
},
|
||||
"Alliance Chieftain": {
|
||||
"hullMass": 400
|
||||
"hullMass": 400,
|
||||
"reserveFuelCapacity": 0.77
|
||||
},
|
||||
"Alliance Crusader": {
|
||||
"hullMass": 500
|
||||
"hullMass": 500,
|
||||
"reserveFuelCapacity": 0.77
|
||||
},
|
||||
"Anaconda": {
|
||||
"hullMass": 400
|
||||
"hullMass": 400,
|
||||
"reserveFuelCapacity": 1.07
|
||||
},
|
||||
"Asp Explorer": {
|
||||
"hullMass": 280
|
||||
"hullMass": 280,
|
||||
"reserveFuelCapacity": 0.63
|
||||
},
|
||||
"Asp Scout": {
|
||||
"hullMass": 150
|
||||
"hullMass": 150,
|
||||
"reserveFuelCapacity": 0.47
|
||||
},
|
||||
"Beluga Liner": {
|
||||
"hullMass": 950
|
||||
"hullMass": 950,
|
||||
"reserveFuelCapacity": 0.81
|
||||
},
|
||||
"Cobra MkIII": {
|
||||
"hullMass": 180
|
||||
"hullMass": 180,
|
||||
"reserveFuelCapacity": 0.49
|
||||
},
|
||||
"Cobra MkIV": {
|
||||
"hullMass": 210
|
||||
"hullMass": 210,
|
||||
"reserveFuelCapacity": 0.51
|
||||
},
|
||||
"Diamondback Explorer": {
|
||||
"hullMass": 260
|
||||
"hullMass": 260,
|
||||
"reserveFuelCapacity": 0.52
|
||||
},
|
||||
"Diamondback Scout": {
|
||||
"hullMass": 170
|
||||
"hullMass": 170,
|
||||
"reserveFuelCapacity": 0.49
|
||||
},
|
||||
"Dolphin": {
|
||||
"hullMass": 140
|
||||
"hullMass": 140,
|
||||
"reserveFuelCapacity": 0.5
|
||||
},
|
||||
"Eagle": {
|
||||
"hullMass": 50
|
||||
"hullMass": 50,
|
||||
"reserveFuelCapacity": 0.34
|
||||
},
|
||||
"Federal Assault Ship": {
|
||||
"hullMass": 480
|
||||
"hullMass": 480,
|
||||
"reserveFuelCapacity": 0.72
|
||||
},
|
||||
"Federal Corvette": {
|
||||
"hullMass": 900
|
||||
"hullMass": 900,
|
||||
"reserveFuelCapacity": 1.13
|
||||
},
|
||||
"Federal Dropship": {
|
||||
"hullMass": 580
|
||||
"hullMass": 580,
|
||||
"reserveFuelCapacity": 0.83
|
||||
},
|
||||
"Federal Gunship": {
|
||||
"hullMass": 580
|
||||
"hullMass": 580,
|
||||
"reserveFuelCapacity": 0.82
|
||||
},
|
||||
"Fer-de-Lance": {
|
||||
"hullMass": 250
|
||||
"hullMass": 250,
|
||||
"reserveFuelCapacity": 0.67
|
||||
},
|
||||
"Hauler": {
|
||||
"hullMass": 14
|
||||
"hullMass": 14,
|
||||
"reserveFuelCapacity": 0.25
|
||||
},
|
||||
"Imperial Clipper": {
|
||||
"hullMass": 400
|
||||
"hullMass": 400,
|
||||
"reserveFuelCapacity": 0.74
|
||||
},
|
||||
"Imperial Courier": {
|
||||
"hullMass": 35
|
||||
"hullMass": 35,
|
||||
"reserveFuelCapacity": 0.41
|
||||
},
|
||||
"Imperial Cutter": {
|
||||
"hullMass": 1100
|
||||
"hullMass": 1100,
|
||||
"reserveFuelCapacity": 1.16
|
||||
},
|
||||
"Imperial Eagle": {
|
||||
"hullMass": 50
|
||||
"hullMass": 50,
|
||||
"reserveFuelCapacity": 0.37
|
||||
},
|
||||
"Keelback": {
|
||||
"hullMass": 180
|
||||
"hullMass": 180,
|
||||
"reserveFuelCapacity": 0.39
|
||||
},
|
||||
"Krait MkII": {
|
||||
"hullMass": 320
|
||||
"hullMass": 320,
|
||||
"reserveFuelCapacity": 0.63
|
||||
},
|
||||
"Krait Phantom": {
|
||||
"hullMass": 270
|
||||
"hullMass": 270,
|
||||
"reserveFuelCapacity": 0.63
|
||||
},
|
||||
"Mamba": {
|
||||
"hullMass": 250
|
||||
"hullMass": 250,
|
||||
"reserveFuelCapacity": 0.5
|
||||
},
|
||||
"Orca": {
|
||||
"hullMass": 290
|
||||
"hullMass": 290,
|
||||
"reserveFuelCapacity": 0.79
|
||||
},
|
||||
"Python": {
|
||||
"hullMass": 350
|
||||
"hullMass": 350,
|
||||
"reserveFuelCapacity": 0.83
|
||||
},
|
||||
"Python Mk II": {
|
||||
"hullMass": 450
|
||||
"hullMass": 450,
|
||||
"reserveFuelCapacity": 0.83
|
||||
},
|
||||
"Sidewinder": {
|
||||
"hullMass": 25
|
||||
"hullMass": 25,
|
||||
"reserveFuelCapacity": 0.3
|
||||
},
|
||||
"Type-10 Defender": {
|
||||
"hullMass": 1200
|
||||
"hullMass": 1200,
|
||||
"reserveFuelCapacity": 0.77
|
||||
},
|
||||
"Type-6 Transporter": {
|
||||
"hullMass": 155
|
||||
"hullMass": 155,
|
||||
"reserveFuelCapacity": 0.39
|
||||
},
|
||||
"Type-7 Transporter": {
|
||||
"hullMass": 350
|
||||
"hullMass": 350,
|
||||
"reserveFuelCapacity": 0.52
|
||||
},
|
||||
"Type-9 Heavy": {
|
||||
"hullMass": 850
|
||||
"hullMass": 850,
|
||||
"reserveFuelCapacity": 0.77
|
||||
},
|
||||
"Viper MkIII": {
|
||||
"hullMass": 50
|
||||
"hullMass": 50,
|
||||
"reserveFuelCapacity": 0.41
|
||||
},
|
||||
"Viper MkIV": {
|
||||
"hullMass": 190
|
||||
"hullMass": 190,
|
||||
"reserveFuelCapacity": 0.46
|
||||
},
|
||||
"Vulture": {
|
||||
"hullMass": 230
|
||||
"hullMass": 230,
|
||||
"reserveFuelCapacity": 0.57
|
||||
}
|
||||
}
|
4
theme.py
4
theme.py
@ -13,7 +13,6 @@ from __future__ import annotations
|
||||
import os
|
||||
import sys
|
||||
import tkinter as tk
|
||||
from os.path import join
|
||||
from tkinter import font as tk_font
|
||||
from tkinter import ttk
|
||||
from typing import Callable
|
||||
@ -39,7 +38,8 @@ if sys.platform == 'win32':
|
||||
AddFontResourceEx.restypes = [LPCWSTR, DWORD, LPCVOID] # type: ignore
|
||||
FR_PRIVATE = 0x10
|
||||
FR_NOT_ENUM = 0x20
|
||||
AddFontResourceEx(join(config.respath, 'EUROCAPS.TTF'), FR_PRIVATE, 0)
|
||||
font_path = config.respath_path / 'EUROCAPS.TTF'
|
||||
AddFontResourceEx(str(font_path), FR_PRIVATE, 0)
|
||||
|
||||
elif sys.platform == 'linux':
|
||||
# pyright: reportUnboundVariable=false
|
||||
|
@ -28,7 +28,6 @@ from tkinter import font as tk_font
|
||||
from tkinter import ttk
|
||||
from typing import Any
|
||||
import plug
|
||||
from os import path
|
||||
from config import config, logger
|
||||
from l10n import translations as tr
|
||||
from monitor import monitor
|
||||
@ -96,7 +95,7 @@ class HyperlinkLabel(tk.Label or ttk.Label): # type: ignore
|
||||
else:
|
||||
# Avoid file length limits if possible
|
||||
target = plug.invoke(url, 'EDSY', 'shipyard_url', loadout, monitor.is_beta)
|
||||
file_name = path.join(config.app_dir_path, "last_shipyard.html")
|
||||
file_name = config.app_dir_path / "last_shipyard.html"
|
||||
|
||||
with open(file_name, 'w') as f:
|
||||
f.write(SHIPYARD_HTML_TEMPLATE.format(
|
||||
@ -194,6 +193,10 @@ class HyperlinkLabel(tk.Label or ttk.Label): # type: ignore
|
||||
menu.add_command(label=tr.tl('Copy'), command=self.copy) # As in Copy and Paste
|
||||
|
||||
if self.name == 'ship':
|
||||
# LANG: Copy the Inara SLEF Format of the active ship to the clipboard
|
||||
menu.add_command(label=tr.tl('Copy Inara SLEF'), command=self.copy_slef, state=tk.DISABLED)
|
||||
menu.entryconfigure(1, state=monitor.slef and tk.NORMAL or tk.DISABLED)
|
||||
|
||||
menu.add_separator()
|
||||
for url in plug.provides('shipyard_url'):
|
||||
menu.add_command(
|
||||
@ -224,3 +227,8 @@ class HyperlinkLabel(tk.Label or ttk.Label): # type: ignore
|
||||
"""Copy the current text to the clipboard."""
|
||||
self.clipboard_clear()
|
||||
self.clipboard_append(self['text'])
|
||||
|
||||
def copy_slef(self) -> None:
|
||||
"""Copy the current text to the clipboard."""
|
||||
self.clipboard_clear()
|
||||
self.clipboard_append(monitor.slef)
|
||||
|
Loading…
x
Reference in New Issue
Block a user