From 4da4afa879a4564e5c05bc1a603a3f3838b38c18 Mon Sep 17 00:00:00 2001 From: Melissa LeBlanc-Williams Date: Fri, 18 Jul 2025 13:21:02 -0700 Subject: [PATCH 1/8] Add template functions and chmod string support --- adafruit_shell.py | 64 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/adafruit_shell.py b/adafruit_shell.py index f45660f..f21c2a3 100644 --- a/adafruit_shell.py +++ b/adafruit_shell.py @@ -23,6 +23,7 @@ Implementation Notes # imports import sys import os +import stat import shutil import subprocess import fcntl @@ -54,6 +55,24 @@ WINDOW_MANAGERS = { "labwc": "W3", } +FILE_MODES = { + "+x": stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH, + "+r": stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH, + "+w": stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH, + "a+x": stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH, + "a+r": stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH, + "a+w": stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH, + "u+x": stat.S_IXUSR, + "u+r": stat.S_IRUSR, + "u+w": stat.S_IWUSR, + "g+x": stat.S_IXGRP, + "g+r": stat.S_IRGRP, + "g+w": stat.S_IWGRP, + "o+x": stat.S_IXOTH, + "o+r": stat.S_IROTH, + "o+w": stat.S_IWOTH, +} + # pylint: disable=too-many-public-methods class Shell: @@ -142,6 +161,44 @@ class Shell: return False return True + def write_templated_file(self, output_path, template, **kwargs): + """ + Use a template file and render it with the given context and write it to the specified path. + The template file should contain placeholders in the format {key} which will be replaced + with the corresponding values from the kwargs dictionary. + """ + # if path is an existing directory, the template filename will be used + output_path = self.path(output_path) + if os.path.isdir(output_path): + output_path = os.path.join(output_path, os.path.basename(template)) + + # Render the template with the provided context + rendered_content = self.load_template(template, **kwargs) + + with open(output_path, "w") as output_file: + output_file.write(rendered_content) + + return True + + def load_template(self, template, **kwargs): + """ + Load a template file and return its content with the placeholders replaced by the provided + context. The template file should contain placeholders in the format {key} which will be + replaced with the corresponding values from the kwargs dictionary. + """ + if not os.path.exists(template): + self.error(f"Template file '{template}' does not exist") + return None + + with open(template, "r") as template_file: + template_content = template_file.read() + + # Render the template with the provided context + for key in kwargs.items(): + template_content = template_content.replace(f"{{{key}}}", str(kwargs[key])) + + return template_content + def info(self, message, **kwargs): """ Display a message with the group in green @@ -417,8 +474,13 @@ class Shell: Change the permissions of a file or directory """ location = self.path(location) + # Convert a text mode to an integer mode + if isinstance(mode, str): + if mode not in FILE_MODES: + raise ValueError(f"Invalid mode string '{mode}'") + mode = FILE_MODES[mode] if not 0 <= mode <= 0o777: - raise ValueError("Invalid mode value") + raise ValueError(f"Invalid mode value '{mode}'") if os.path.exists(location): os.chmod(location, mode) From 7bc3e7430985a59d9bd49cfd7ce0afe5611f926f Mon Sep 17 00:00:00 2001 From: Melissa LeBlanc-Williams Date: Fri, 18 Jul 2025 15:16:27 -0700 Subject: [PATCH 2/8] Fixed bug introduced by pylint suggestion --- adafruit_shell.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/adafruit_shell.py b/adafruit_shell.py index f21c2a3..68f97e7 100644 --- a/adafruit_shell.py +++ b/adafruit_shell.py @@ -194,8 +194,8 @@ class Shell: template_content = template_file.read() # Render the template with the provided context - for key in kwargs.items(): - template_content = template_content.replace(f"{{{key}}}", str(kwargs[key])) + for key, value in kwargs.items(): + template_content = template_content.replace(f"{{{key}}}", str(value)) return template_content From 897d6ddbe8e6eec3bfd80aa22ab8520a70c23c32 Mon Sep 17 00:00:00 2001 From: Melissa LeBlanc-Williams Date: Mon, 4 Aug 2025 12:41:42 -0700 Subject: [PATCH 3/8] Add window manager detection --- adafruit_shell.py | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/adafruit_shell.py b/adafruit_shell.py index 68f97e7..8277713 100644 --- a/adafruit_shell.py +++ b/adafruit_shell.py @@ -51,7 +51,7 @@ RASPI_VERSIONS = ( WINDOW_MANAGERS = { "x11": "W1", - "wayland": "W2", + "wayfire": "W2", "labwc": "W3", } @@ -175,6 +175,12 @@ class Shell: # Render the template with the provided context rendered_content = self.load_template(template, **kwargs) + if rendered_content is None: + self.error( + f"Failed to load template '{template}'. Unable to write file '{output_path}'." + ) + return False + with open(output_path, "w") as output_file: output_file.write(rendered_content) @@ -718,6 +724,32 @@ class Shell: ): raise RuntimeError("Unable to change window manager") + def get_window_manager(self): + """ + Get the current window manager + """ + sessions = {"wayfire": "LXDE-pi-wayfire"} + # Check for Raspbian Desktop sessions + if self.exists("/usr/share/xsessions/rpd-x.desktop") or self.exists( + "/usr/share/wayland-sessions/rpd-labwc.desktop" + ): + sessions = {"x11": "rpd-x", "labwc": "rpd-labwc"} + else: + sessions = { + "x11": "LXDE-pi-x", + "labwc": "LXDE-pi-labwc", + } + + matches = self.pattern_search( + "/etc/lightdm/lightdm.conf", "^(?!#.*?)user-session=(.+)", False, True + ) + if matches: + session_match = matches.group(1) + for key, session in session_match.items(): + if session == sessions[key]: + return key + return None + def get_boot_config(self): """ Get the location of the boot config file From 45b0cafa2b2b71a2010e3b1d677643b37c60750e Mon Sep 17 00:00:00 2001 From: Melissa LeBlanc-Williams Date: Mon, 4 Aug 2025 12:52:49 -0700 Subject: [PATCH 4/8] Fix error from pre-commit suggestion --- adafruit_shell.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/adafruit_shell.py b/adafruit_shell.py index 8277713..295674b 100644 --- a/adafruit_shell.py +++ b/adafruit_shell.py @@ -745,8 +745,8 @@ class Shell: ) if matches: session_match = matches.group(1) - for key, session in session_match.items(): - if session == sessions[key]: + for key, session in sessions.items(): + if session_match == session: return key return None From 1522b71317a783661e3b4f7744b12d4d43a92fe9 Mon Sep 17 00:00:00 2001 From: Melissa LeBlanc-Williams Date: Wed, 6 Aug 2025 10:13:38 -0700 Subject: [PATCH 5/8] Use class write funtion for appending --- adafruit_shell.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/adafruit_shell.py b/adafruit_shell.py index 295674b..28f1a75 100644 --- a/adafruit_shell.py +++ b/adafruit_shell.py @@ -181,8 +181,8 @@ class Shell: ) return False - with open(output_path, "w") as output_file: - output_file.write(rendered_content) + append = kwargs.get("append", False) + self.write_text_file(output_path, rendered_content, append=append) return True From 7fc4630f1d1682945b85de59f23fcbad7a9869d2 Mon Sep 17 00:00:00 2001 From: Melissa LeBlanc-Williams Date: Wed, 6 Aug 2025 11:39:58 -0700 Subject: [PATCH 6/8] Fix wayland dm detection --- adafruit_shell.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/adafruit_shell.py b/adafruit_shell.py index 28f1a75..fb67537 100644 --- a/adafruit_shell.py +++ b/adafruit_shell.py @@ -733,12 +733,9 @@ class Shell: if self.exists("/usr/share/xsessions/rpd-x.desktop") or self.exists( "/usr/share/wayland-sessions/rpd-labwc.desktop" ): - sessions = {"x11": "rpd-x", "labwc": "rpd-labwc"} + sessions.update({"x11": "rpd-x", "labwc": "rpd-labwc"}) else: - sessions = { - "x11": "LXDE-pi-x", - "labwc": "LXDE-pi-labwc", - } + sessions.update({"x11": "LXDE-pi-x", "labwc": "LXDE-pi-labwc"}) matches = self.pattern_search( "/etc/lightdm/lightdm.conf", "^(?!#.*?)user-session=(.+)", False, True From 0a414a23426d92dae5ff8d5eaa10cb426ddd13f0 Mon Sep 17 00:00:00 2001 From: Melissa LeBlanc-Williams Date: Wed, 6 Aug 2025 13:50:52 -0700 Subject: [PATCH 7/8] Add read_text_file function --- adafruit_shell.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/adafruit_shell.py b/adafruit_shell.py index fb67537..d33c502 100644 --- a/adafruit_shell.py +++ b/adafruit_shell.py @@ -543,6 +543,16 @@ class Shell: with open(self.path(path), mode, encoding="utf-8") as service_file: service_file.write(content) + def read_text_file(self, path): + """ + Read the contents of a file at the specified path + """ + path = self.path(path) + if not os.path.exists(path): + raise FileNotFoundError(f"File '{path}' does not exist") + with open(path, "r", encoding="utf-8") as file: + return file.read() + @staticmethod def is_python3(): "Check if we are running Python 3 or later" From 01be1e437d3c06555fcc21d43c44f3ff344d3b87 Mon Sep 17 00:00:00 2001 From: Melissa LeBlanc-Williams Date: Wed, 6 Aug 2025 14:58:27 -0700 Subject: [PATCH 8/8] Allow finding multiple items --- adafruit_shell.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/adafruit_shell.py b/adafruit_shell.py index d33c502..cd7bc24 100644 --- a/adafruit_shell.py +++ b/adafruit_shell.py @@ -384,7 +384,10 @@ class Shell: # Not found; append (silently) self.write_text_file(file, replacement, append=True) - def pattern_search(self, location, pattern, multi_line=False, return_match=False): + # pylint: disable=too-many-arguments + def pattern_search( + self, location, pattern, multi_line=False, return_match=False, find_all=False + ): """ Similar to grep, but uses pure python multi_line will search the entire file as a large text glob, @@ -394,16 +397,17 @@ class Shell: """ location = self.path(location) found = False + search_function = re.findall if find_all else re.search if self.exists(location) and not self.isdir(location): if multi_line: with open(location, "r+", encoding="utf-8") as file: - match = re.search(pattern, file.read(), flags=re.DOTALL) + match = search_function(pattern, file.read(), flags=re.DOTALL) if match: found = True else: for line in fileinput.FileInput(location): - match = re.search(pattern, line) + match = search_function(pattern, line) if match: found = True break