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( argumentParser.add_argument(
'-s', '--setting', '-s', '--setting',
metavar='SETTING-FILE', metavar='SETTING-FILE',
default='/etc/fenrir/settings/settings.conf', default=None,
help='Path to custom settings file' help='Path to custom settings file'
) )
argumentParser.add_argument( argumentParser.add_argument(
@@ -22,10 +22,7 @@ class command:
return _("Saves your current Fenrir settings so they are the default.") return _("Saves your current Fenrir settings so they are the default.")
def run(self): def run(self):
settings_file = self.env["runtime"][ self.env["runtime"]["SettingsManager"].save_settings()
"SettingsManager"
].get_settings_file()
self.env["runtime"]["SettingsManager"].save_settings(settings_file)
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
_("Settings saved."), interrupt=True _("Settings saved."), interrupt=True
) )
@@ -48,8 +48,7 @@ class DynamicKeyboardLayoutCommand:
) )
# Save to the actual config file # Save to the actual config file
configFilePath = settingsManager.get_settings_file() settingsManager.save_settings()
settingsManager.save_settings(configFilePath)
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
f"Keyboard layout set to {self.layoutName}. Please restart Fenrir for this change to take effect." 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): 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( self.env["runtime"]["SettingsManager"].save_settings(
setting_config_path setting_config_path
) )
+94 -21
View File
@@ -42,11 +42,18 @@ fenrir_path = os.path.dirname(currentdir)
class SettingsManager: 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): def __init__(self):
self.settings = settings_data self.settings = settings_data
self.settingArgDict = {} self.settingArgDict = {}
self.bindingsBackup = None self.bindingsBackup = None
self.settings_file = "" self.settings_file = ""
self.save_settings_path = ""
def initialize(self, environment): def initialize(self, environment):
self.env = environment self.env = environment
@@ -104,12 +111,57 @@ class SettingsManager:
return self.settings_file return self.settings_file
def set_settings_file(self, settings_file): def set_settings_file(self, settings_file):
if not os.path.exists(settings_file): if os.path.exists(settings_file) and not os.access(
return settings_file, os.R_OK
if not os.access(settings_file, os.R_OK): ):
return return
self.settings_file = settings_file 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): def load_settings(self, setting_config_path):
if not os.path.exists(setting_config_path): if not os.path.exists(setting_config_path):
return False return False
@@ -128,7 +180,29 @@ class SettingsManager:
self.set_settings_file(setting_config_path) self.set_settings_file(setting_config_path)
return True 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 # set opt dict here
# save file # save file
try: try:
@@ -458,25 +532,24 @@ class SettingsManager:
def init_fenrir_config( def init_fenrir_config(
self, cliArgs, fenrir_manager=None, environment=environment.environment self, cliArgs, fenrir_manager=None, environment=environment.environment
): ):
settings_root = "/etc/fenrirscreenreader/" self.cliArgs = cliArgs
settings_file = cliArgs.setting
settings_root = self.get_resource_settings_root()
if not settings_root:
return None
sound_root = "/usr/share/sounds/fenrirscreenreader/" 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 not os.path.exists(sound_root):
if os.path.exists(fenrir_path + "/../../config/sound/"): bundled_sound_root = os.path.join(
sound_root = fenrir_path + "/../../config/sound/" 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"] = self
environment["runtime"]["SettingsManager"].initialize(environment) environment["runtime"]["SettingsManager"].initialize(environment)
+1 -1
View File
@@ -4,5 +4,5 @@
# Fenrir TTY screen reader # Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors. # By Chrys, Storm Dragon, and contributors.
version = "2026.05.10" version = "2026.05.12"
code_name = "testing" code_name = "testing"
@@ -288,6 +288,20 @@ class driver(screenDriver):
msg_bytes = bytes(msg_bytes, "UTF-8") msg_bytes = bytes(msg_bytes, "UTF-8")
os.write(screen, msg_bytes) 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): def get_session_information(self):
self.env["screen"]["autoIgnoreScreens"] = [] self.env["screen"]["autoIgnoreScreens"] = []
self.env["general"]["prev_user"] = getpass.getuser() self.env["general"]["prev_user"] = getpass.getuser()
@@ -420,6 +434,7 @@ class driver(screenDriver):
) )
break break
try: try:
self.interrupt_output_on_stdin_input(msg_bytes)
self.inject_text_to_screen(msg_bytes) self.inject_text_to_screen(msg_bytes)
except Exception as e: except Exception as e:
self.env["runtime"][ self.env["runtime"][
+57
View File
@@ -1,4 +1,5 @@
import pytest import pytest
from unittest.mock import Mock
from fenrirscreenreader.screenDriver.ptyDriver import PTYConstants from fenrirscreenreader.screenDriver.ptyDriver import PTYConstants
from fenrirscreenreader.screenDriver.ptyDriver import Terminal from fenrirscreenreader.screenDriver.ptyDriver import Terminal
@@ -52,3 +53,59 @@ def test_optional_float_setting_uses_default_when_missing():
) )
== PTYConstants.OUTPUT_READ_TIMEOUT == 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 pytest
import sys import sys
from pathlib import Path from pathlib import Path
from unittest.mock import Mock
# Import the settings manager # Import the settings manager
from fenrirscreenreader.core.settingsData import settings_data from fenrirscreenreader.core.settingsData import settings_data
from fenrirscreenreader.core.settingsManager import SettingsManager from fenrirscreenreader.core.settingsManager import SettingsManager
from fenrirscreenreader.commands.commands import save_settings
@pytest.mark.unit @pytest.mark.unit
@@ -198,3 +200,93 @@ class TestValidationSkipsUnknownSettings:
def test_focus_settings_define_tui_toggle(): def test_focus_settings_define_tui_toggle():
"""Focus settings should include the TUI toggle used by on-screen handlers.""" """Focus settings should include the TUI toggle used by on-screen handlers."""
assert settings_data["focus"]["tui"] is False 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()