Implemented local settings save in ~/.local/share/stormux. Also, I think I got interrupt on any keypress working better when using -x.

This commit is contained in:
Storm Dragon
2026-05-12 02:06:07 -04:00
parent 9e2d5a89b3
commit 57980225ad
9 changed files with 262 additions and 35 deletions
+1 -1
View File
@@ -41,7 +41,7 @@ def create_argument_parser():
argumentParser.add_argument(
'-s', '--setting',
metavar='SETTING-FILE',
default='/etc/fenrir/settings/settings.conf',
default=None,
help='Path to custom settings file'
)
argumentParser.add_argument(
@@ -22,10 +22,7 @@ class command:
return _("Saves your current Fenrir settings so they are the default.")
def run(self):
settings_file = self.env["runtime"][
"SettingsManager"
].get_settings_file()
self.env["runtime"]["SettingsManager"].save_settings(settings_file)
self.env["runtime"]["SettingsManager"].save_settings()
self.env["runtime"]["OutputManager"].present_text(
_("Settings saved."), interrupt=True
)
@@ -48,8 +48,7 @@ class DynamicKeyboardLayoutCommand:
)
# Save to the actual config file
configFilePath = settingsManager.get_settings_file()
settingsManager.save_settings(configFilePath)
settingsManager.save_settings()
self.env["runtime"]["OutputManager"].present_text(
f"Keyboard layout set to {self.layoutName}. Please restart Fenrir for this change to take effect."
@@ -464,12 +464,6 @@ class RemoteManager:
)
def save_settings(self, setting_config_path=None):
if not setting_config_path:
setting_config_path = self.env["runtime"][
"SettingsManager"
].get_settings_file()
if setting_config_path == "":
return
self.env["runtime"]["SettingsManager"].save_settings(
setting_config_path
)
+94 -21
View File
@@ -42,11 +42,18 @@ fenrir_path = os.path.dirname(currentdir)
class SettingsManager:
system_settings_root = "/etc/fenrirscreenreader/"
system_settings_file = "/etc/fenrirscreenreader/settings/settings.conf"
user_settings_file = (
"~/.local/share/stormux/fenrirscreenreader/settings/settings.conf"
)
def __init__(self):
self.settings = settings_data
self.settingArgDict = {}
self.bindingsBackup = None
self.settings_file = ""
self.save_settings_path = ""
def initialize(self, environment):
self.env = environment
@@ -104,12 +111,57 @@ class SettingsManager:
return self.settings_file
def set_settings_file(self, settings_file):
if not os.path.exists(settings_file):
return
if not os.access(settings_file, os.R_OK):
if os.path.exists(settings_file) and not os.access(
settings_file, os.R_OK
):
return
self.settings_file = settings_file
def get_user_settings_file(self):
return os.path.expanduser(self.user_settings_file)
def get_system_settings_file(self):
return self.system_settings_file
def is_user_settings_mode(self):
return os.geteuid() != 0
def get_default_save_settings_file(self):
if self.is_user_settings_mode():
return self.get_user_settings_file()
return self.get_system_settings_file()
def get_bundled_settings_root(self):
return os.path.abspath(os.path.join(fenrir_path, "../../config/")) + "/"
def get_resource_settings_root(self):
if os.path.exists(self.system_settings_root):
return self.system_settings_root
bundled_settings_root = self.get_bundled_settings_root()
if os.path.exists(bundled_settings_root):
return bundled_settings_root
return ""
def resolve_settings_file(self, requested_settings_file=None):
if requested_settings_file and os.path.exists(requested_settings_file):
return requested_settings_file
if self.is_user_settings_mode() and os.path.exists(
self.get_user_settings_file()
):
return self.get_user_settings_file()
if os.path.exists(self.get_system_settings_file()):
return self.get_system_settings_file()
bundled_settings_file = os.path.join(
self.get_bundled_settings_root(), "settings/settings.conf"
)
if os.path.exists(bundled_settings_file):
return bundled_settings_file
return ""
def load_settings(self, setting_config_path):
if not os.path.exists(setting_config_path):
return False
@@ -128,7 +180,29 @@ class SettingsManager:
self.set_settings_file(setting_config_path)
return True
def save_settings(self, setting_config_path):
def save_settings(self, setting_config_path=None):
if setting_config_path is None:
if self.save_settings_path:
setting_config_path = self.save_settings_path
else:
setting_config_path = self.get_default_save_settings_file()
# Ensure directory exists for user-local settings
if setting_config_path and setting_config_path.startswith(
os.path.expanduser("~/.local/")
):
config_dir = os.path.dirname(setting_config_path)
if not os.path.exists(config_dir):
try:
os.makedirs(config_dir, mode=0o755, exist_ok=True)
except Exception as e:
self.env["runtime"]["DebugManager"].write_debug_out(
"save_settings: failed to create directory "
+ config_dir
+ ": "
+ str(e),
debug.DebugLevel.ERROR,
)
# set opt dict here
# save file
try:
@@ -458,25 +532,24 @@ class SettingsManager:
def init_fenrir_config(
self, cliArgs, fenrir_manager=None, environment=environment.environment
):
settings_root = "/etc/fenrirscreenreader/"
settings_file = cliArgs.setting
self.cliArgs = cliArgs
settings_root = self.get_resource_settings_root()
if not settings_root:
return None
sound_root = "/usr/share/sounds/fenrirscreenreader/"
# get fenrir settings root
if not os.path.exists(settings_root):
if os.path.exists(fenrir_path + "/../../config/"):
settings_root = fenrir_path + "/../../config/"
else:
return None
# get settings file
if settings_file is None or not os.path.exists(settings_file):
if os.path.exists(settings_root + "/settings/settings.conf"):
settings_file = settings_root + "/settings/settings.conf"
else:
return None
# get sound themes root
if not os.path.exists(sound_root):
if os.path.exists(fenrir_path + "/../../config/sound/"):
sound_root = fenrir_path + "/../../config/sound/"
bundled_sound_root = os.path.join(
self.get_bundled_settings_root(), "sound/"
)
if os.path.exists(bundled_sound_root):
sound_root = bundled_sound_root
self.save_settings_path = self.get_default_save_settings_file()
settings_file = self.resolve_settings_file(cliArgs.setting)
if not settings_file:
return None
environment["runtime"]["SettingsManager"] = self
environment["runtime"]["SettingsManager"].initialize(environment)
+1 -1
View File
@@ -4,5 +4,5 @@
# Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors.
version = "2026.05.10"
version = "2026.05.12"
code_name = "testing"
@@ -288,6 +288,20 @@ class driver(screenDriver):
msg_bytes = bytes(msg_bytes, "UTF-8")
os.write(screen, msg_bytes)
def interrupt_output_on_stdin_input(self, msg_bytes):
if not msg_bytes:
return
settings_manager = self.env["runtime"]["SettingsManager"]
if not settings_manager.get_setting_as_bool(
"keyboard", "interrupt_on_key_press"
):
return
if settings_manager.get_setting(
"keyboard", "interrupt_on_key_press_filter"
).strip():
return
self.env["runtime"]["OutputManager"].interrupt_output()
def get_session_information(self):
self.env["screen"]["autoIgnoreScreens"] = []
self.env["general"]["prev_user"] = getpass.getuser()
@@ -420,6 +434,7 @@ class driver(screenDriver):
)
break
try:
self.interrupt_output_on_stdin_input(msg_bytes)
self.inject_text_to_screen(msg_bytes)
except Exception as e:
self.env["runtime"][
+57
View File
@@ -1,4 +1,5 @@
import pytest
from unittest.mock import Mock
from fenrirscreenreader.screenDriver.ptyDriver import PTYConstants
from fenrirscreenreader.screenDriver.ptyDriver import Terminal
@@ -52,3 +53,59 @@ def test_optional_float_setting_uses_default_when_missing():
)
== PTYConstants.OUTPUT_READ_TIMEOUT
)
@pytest.mark.unit
def test_pty_stdin_input_interrupts_output_when_all_keys_interrupt_enabled():
pty_driver = PtyDriver()
settings_manager = Mock()
settings_manager.get_setting_as_bool.return_value = True
settings_manager.get_setting.return_value = ""
output_manager = Mock()
pty_driver.env = {
"runtime": {
"SettingsManager": settings_manager,
"OutputManager": output_manager,
}
}
pty_driver.interrupt_output_on_stdin_input(b"a")
output_manager.interrupt_output.assert_called_once_with()
@pytest.mark.unit
def test_pty_stdin_input_honors_interrupt_disabled():
pty_driver = PtyDriver()
settings_manager = Mock()
settings_manager.get_setting_as_bool.return_value = False
output_manager = Mock()
pty_driver.env = {
"runtime": {
"SettingsManager": settings_manager,
"OutputManager": output_manager,
}
}
pty_driver.interrupt_output_on_stdin_input(b"a")
output_manager.interrupt_output.assert_not_called()
@pytest.mark.unit
def test_pty_stdin_input_leaves_filtered_interrupts_to_key_events():
pty_driver = PtyDriver()
settings_manager = Mock()
settings_manager.get_setting_as_bool.return_value = True
settings_manager.get_setting.return_value = "KEY_ENTER"
output_manager = Mock()
pty_driver.env = {
"runtime": {
"SettingsManager": settings_manager,
"OutputManager": output_manager,
}
}
pty_driver.interrupt_output_on_stdin_input(b"a")
output_manager.interrupt_output.assert_not_called()
+92
View File
@@ -8,10 +8,12 @@ for all configurable settings that could cause crashes or accessibility issues.
import pytest
import sys
from pathlib import Path
from unittest.mock import Mock
# Import the settings manager
from fenrirscreenreader.core.settingsData import settings_data
from fenrirscreenreader.core.settingsManager import SettingsManager
from fenrirscreenreader.commands.commands import save_settings
@pytest.mark.unit
@@ -198,3 +200,93 @@ class TestValidationSkipsUnknownSettings:
def test_focus_settings_define_tui_toggle():
"""Focus settings should include the TUI toggle used by on-screen handlers."""
assert settings_data["focus"]["tui"] is False
@pytest.mark.unit
@pytest.mark.settings
class TestSettingsPathSelection:
"""Test root/user settings load and save path selection."""
def setup_method(self):
self.manager = SettingsManager()
def configure_paths(self, tmp_path):
system_root = tmp_path / "etc" / "fenrirscreenreader"
system_file = system_root / "settings" / "settings.conf"
user_file = (
tmp_path
/ "home"
/ "Username"
/ ".local"
/ "share"
/ "stormux"
/ "fenrirscreenreader"
/ "settings"
/ "settings.conf"
)
self.manager.system_settings_root = str(system_root) + "/"
self.manager.system_settings_file = str(system_file)
self.manager.user_settings_file = str(user_file)
return system_file, user_file
def test_non_root_loads_user_settings_when_present(
self, tmp_path, monkeypatch
):
system_file, user_file = self.configure_paths(tmp_path)
system_file.parent.mkdir(parents=True)
system_file.write_text("[general]\n", encoding="utf-8")
user_file.parent.mkdir(parents=True)
user_file.write_text("[general]\n", encoding="utf-8")
monkeypatch.setattr("os.geteuid", lambda: 1000)
assert self.manager.resolve_settings_file() == str(user_file)
def test_non_root_falls_back_to_system_settings(
self, tmp_path, monkeypatch
):
system_file, _user_file = self.configure_paths(tmp_path)
system_file.parent.mkdir(parents=True)
system_file.write_text("[general]\n", encoding="utf-8")
monkeypatch.setattr("os.geteuid", lambda: 1000)
assert self.manager.resolve_settings_file() == str(system_file)
def test_root_uses_system_settings_even_when_user_settings_exists(
self, tmp_path, monkeypatch
):
system_file, user_file = self.configure_paths(tmp_path)
system_file.parent.mkdir(parents=True)
system_file.write_text("[general]\n", encoding="utf-8")
user_file.parent.mkdir(parents=True)
user_file.write_text("[general]\n", encoding="utf-8")
monkeypatch.setattr("os.geteuid", lambda: 0)
assert self.manager.resolve_settings_file() == str(system_file)
def test_save_default_path_follows_effective_user(
self, tmp_path, monkeypatch
):
system_file, user_file = self.configure_paths(tmp_path)
monkeypatch.setattr("os.geteuid", lambda: 1000)
assert self.manager.get_default_save_settings_file() == str(user_file)
monkeypatch.setattr("os.geteuid", lambda: 0)
assert self.manager.get_default_save_settings_file() == str(system_file)
def test_save_settings_command_uses_default_save_path(self):
manager = Mock()
output_manager = Mock()
command = save_settings.command()
command.initialize(
{
"runtime": {
"SettingsManager": manager,
"OutputManager": output_manager,
}
}
)
command.run()
manager.save_settings.assert_called_once_with()