Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 6026f5f

Browse files
lucasssvazme-no-dev
andauthoredApr 8, 2024
Add configuration UI (espressif#158)
* Initial project template * Fix EOF * Simplify * Test * Fix * Improve Screens * Add placeholder Readme * Remove unused imports * Fix CWD * Fix target selection * Misc improvements * WIP compile screen * Update * Fix command * Finish compile screen * Added comments and improvements * Run on task * Make style a bit more Arduino-esque * Add CLI arguments * Small improvements * Rename * Add custom widgets * Fix widgets * Add remaining settings * Improve style * Improve compilation screen * Handle exception * Add no copy argument * Replace "arch" with "uname -m" * Fix copy argument * Fix arg message * Fix chdir * Improve header * Properly kill subprocess * Fix compilation issues * Hide setting depending on switch state * QoL improvements * Improve style * Add python version check * Separate main menu screen * Fix quit and improve logs * Propagate exit code * Rename quit to exit for consistency * Add readme * Improve Readme * Add keybindings * Improve readme * Revert "Merge branch 'bugfix/arch_cmd' into feature/config_ui" This reverts commit 31dbd36, reversing changes made to 2d8e462. * Target list based on JSON and support for any combination of targets * Improve documentation * Add note to the documentation * Fix copy after compilation * Fix bugs * Improve documentation --------- Co-authored-by: me-no-dev <[email protected]> Co-authored-by: Me No Dev <[email protected]>
1 parent 4ec4d25 commit 6026f5f

File tree

9 files changed

+1039
-0
lines changed

9 files changed

+1039
-0
lines changed
 

‎README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,32 @@ git clone https://github.com/espressif/esp32-arduino-lib-builder
1212
cd esp32-arduino-lib-builder
1313
./build.sh
1414
```
15+
16+
### Using the User Interface
17+
18+
You can more easily build the libraries using the user interface found in the `tools/config_editor/` folder.
19+
It is a Python script that allows you to select and edit the options for the libraries you want to build.
20+
The script has mouse support and can also be pre-configured using the same command line arguments as the `build.sh` script.
21+
For more information and troubleshooting, please refer to the [UI README](tools/config_editor/README.md).
22+
23+
To use it, follow these steps:
24+
25+
1. Make sure you have the required dependencies installed:
26+
- Python 3.9 or later
27+
- The [Textual](https://github.com/textualize/textual/) library
28+
- All the dependencies listed in the previous section
29+
30+
2. Execute the script `tools/config_editor/app.py` from any folder. It will automatically detect the path to the root of the repository.
31+
32+
3. Configure the compilation and ESP-IDF options as desired.
33+
34+
4. Click on the "Compile Static Libraries" button to start the compilation process.
35+
36+
5. The script will show the compilation output in a new screen. Note that the compilation process can take many hours, depending on the number of libraries selected and the options chosen.
37+
38+
6. If the compilation is successful and the option to copy the libraries to the Arduino Core folder is enabled, it will already be available for use in the Arduino IDE. Otherwise, you can find the compiled libraries in the `esp32-arduino-libs` folder alongside this repository.
39+
- Note that the copy operation doesn't currently support the core downloaded from the Arduino IDE Boards Manager, only the manual installation from the [`arduino-esp32`](https://github.com/espressif/arduino-esp32) repository.
40+
1541
### Documentation
1642

1743
For more information about how to use the Library builder, please refer to this [Documentation page](https://docs.espressif.com/projects/arduino-esp32/en/latest/lib_builder.html?highlight=lib%20builder)

‎tools/config_editor/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.venv/
2+
__pycache__/

‎tools/config_editor/README.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Arduino Static Libraries Configuration Editor
2+
3+
This is a simple application to configure the static libraries for the ESP32 Arduino core.
4+
It allows the user to select the targets to compile, change the configuration options and compile the libraries.
5+
It has mouse support and can be pre-configured using command line arguments.
6+
7+
## Requirements
8+
- Python 3.9 or later
9+
- The "textual" library (install it using `pip install textual`)
10+
- The requirements from esp32-arduino-lib-builder
11+
12+
## Troubleshooting
13+
14+
In some cases, the UI might not look as expected. This can happen due to the terminal emulator not supporting the required features.
15+
16+
### WSL
17+
18+
If you are using WSL, it is recommended to use the Windows Terminal to visualize the application. Otherwise, the application layout and colors might not be displayed correctly.
19+
The Windows Terminal can be installed from the Microsoft Store.
20+
21+
### MacOS
22+
23+
If you are using MacOS and the application looks weird, check [this guide from Textual](https://textual.textualize.io/FAQ/#why-doesnt-textual-look-good-on-macos) to fix it.
24+
25+
## Usage
26+
27+
These command line arguments can be used to pre-configure the application:
28+
29+
Command line arguments:
30+
-t, --target <target> Comma-separated list of targets to be compiled.
31+
Choose from: all, esp32, esp32s2, esp32s3, esp32c2, esp32c3, esp32c6, esp32h2. Default: all except esp32c2
32+
--copy, --no-copy Enable/disable copying the compiled libraries to arduino-esp32. Enabled by default
33+
-c, --arduino-path <path> Path to arduino-esp32 directory. Default: OS dependent
34+
-A, --arduino-branch <branch> Branch of the arduino-esp32 repository to be used. Default: set by the build script
35+
-I, --idf-branch <branch> Branch of the ESP-IDF repository to be used. Default: set by the build script
36+
-i, --idf-commit <commit> Commit of the ESP-IDF repository to be used. Default: set by the build script
37+
-D, --debug-level <level> Debug level to be set to ESP-IDF.
38+
Choose from: default, none, error, warning, info, debug, verbose. Default: default

‎tools/config_editor/app.py

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
#!/usr/bin/env python
2+
3+
"""
4+
Arduino Static Libraries Configuration Editor
5+
6+
This is a simple application to configure the static libraries for the ESP32 Arduino core.
7+
It allows the user to select the targets to compile, change the configuration options and compile the libraries.
8+
9+
Requires Python 3.9 or later.
10+
11+
The application is built using the "textual" library, which is a Python library for building text-based user interfaces.
12+
13+
Note that this application still needs the requirements from esp32-arduino-lib-builder to be installed.
14+
15+
Command line arguments:
16+
-t, --target <target> Comma-separated list of targets to be compiled.
17+
Choose from: all, esp32, esp32s2, esp32s3, esp32c2, esp32c3, esp32c6, esp32h2. Default: all except esp32c2
18+
--copy, --no-copy Enable/disable copying the compiled libraries to arduino-esp32. Enabled by default
19+
-c, --arduino-path <path> Path to arduino-esp32 directory. Default: OS dependent
20+
-A, --arduino-branch <branch> Branch of the arduino-esp32 repository to be used. Default: set by the build script
21+
-I, --idf-branch <branch> Branch of the ESP-IDF repository to be used. Default: set by the build script
22+
-i, --idf-commit <commit> Commit of the ESP-IDF repository to be used. Default: set by the build script
23+
-D, --debug-level <level> Debug level to be set to ESP-IDF.
24+
Choose from: default, none, error, warning, info, debug, verbose. Default: default
25+
26+
"""
27+
28+
import argparse
29+
import json
30+
import os
31+
import platform
32+
import sys
33+
34+
from pathlib import Path
35+
36+
try:
37+
from textual.app import App, ComposeResult
38+
from textual.binding import Binding
39+
from textual.containers import VerticalScroll
40+
from textual.screen import Screen
41+
from textual.widgets import Button, Header, Label, Footer
42+
except ImportError:
43+
print("Please install the \"textual\" package before running this script.")
44+
exit(1)
45+
46+
from settings import SettingsScreen
47+
from editor import EditorScreen
48+
from compile import CompileScreen
49+
50+
class MainScreen(Screen):
51+
# Main screen class
52+
53+
# Set the key bindings
54+
BINDINGS = [
55+
Binding("c", "app.push_screen('compile')", "Compile"),
56+
Binding("e", "app.push_screen('editor')", "Editor"),
57+
Binding("s", "app.push_screen('settings')", "Settings"),
58+
Binding("q", "app.quit", "Quit"),
59+
]
60+
61+
def on_button_pressed(self, event: Button.Pressed) -> None:
62+
# Event handler called when a button is pressed
63+
if event.button.id == "compile-button":
64+
print("Compile button pressed")
65+
self.app.push_screen("compile")
66+
elif event.button.id == "settings-button":
67+
print("Settings button pressed")
68+
self.app.push_screen("settings")
69+
elif event.button.id == "editor-button":
70+
print("Editor button pressed")
71+
self.app.push_screen("editor")
72+
elif event.button.id == "quit-button":
73+
print("Quit button pressed")
74+
self.app.exit()
75+
76+
def compose(self) -> ComposeResult:
77+
# Compose main menu
78+
yield Header()
79+
with VerticalScroll(id="main-menu-container"):
80+
yield Label("ESP32 Arduino Static Libraries Configuration Editor", id="main-menu-title")
81+
yield Button("Compile Static Libraries", id="compile-button", classes="main-menu-button")
82+
yield Button("Sdkconfig Editor", id="editor-button", classes="main-menu-button")
83+
yield Button("Settings", id="settings-button", classes="main-menu-button")
84+
yield Button("Quit", id="quit-button", classes="main-menu-button")
85+
yield Footer()
86+
87+
def on_mount(self) -> None:
88+
# Event handler called when the app is mounted for the first time
89+
self.title = "Configurator"
90+
self.sub_title = "Main Menu"
91+
print("Main screen mounted.")
92+
93+
class ConfigEditorApp(App):
94+
# Main application class
95+
96+
# Set the root and script paths
97+
SCRIPT_PATH = os.path.abspath(os.path.dirname(__file__))
98+
ROOT_PATH = os.path.abspath(os.path.join(SCRIPT_PATH, "..", ".."))
99+
100+
# Set the application options
101+
supported_targets = []
102+
setting_enable_copy = True
103+
104+
# Options to be set by the command line arguments
105+
setting_target = ""
106+
setting_arduino_path = ""
107+
setting_arduino_branch = ""
108+
setting_idf_branch = ""
109+
setting_idf_commit = ""
110+
setting_debug_level = ""
111+
112+
ENABLE_COMMAND_PALETTE = False
113+
CSS_PATH = "style.tcss"
114+
SCREENS = {
115+
"main": MainScreen(),
116+
"settings": SettingsScreen(),
117+
"compile": CompileScreen(),
118+
"editor": EditorScreen(),
119+
}
120+
121+
def on_mount(self) -> None:
122+
print("Application mounted. Initial options:")
123+
print("Python version: " + sys.version)
124+
print("Root path: " + self.ROOT_PATH)
125+
print("Script path: " + self.SCRIPT_PATH)
126+
print("Supported Targets: " + ", ".join(self.supported_targets))
127+
print("Default targets: " + self.setting_target)
128+
print("Enable Copy: " + str(self.setting_enable_copy))
129+
print("Arduino Path: " + str(self.setting_arduino_path))
130+
print("Arduino Branch: " + str(self.setting_arduino_branch))
131+
print("IDF Branch: " + str(self.setting_idf_branch))
132+
print("IDF Commit: " + str(self.setting_idf_commit))
133+
print("IDF Debug Level: " + str(self.setting_debug_level))
134+
self.push_screen("main")
135+
136+
def arduino_default_path():
137+
sys_name = platform.system()
138+
home = str(Path.home())
139+
if sys_name == "Linux":
140+
return os.path.join(home, "Arduino", "hardware", "espressif", "esp32")
141+
else: # Windows and MacOS
142+
return os.path.join(home, "Documents", "Arduino", "hardware", "espressif", "esp32")
143+
144+
def check_arduino_path(path):
145+
return os.path.isdir(path)
146+
147+
def main() -> None:
148+
# Set the PYTHONUNBUFFERED environment variable to "1" to disable the output buffering
149+
os.environ['PYTHONUNBUFFERED'] = "1"
150+
151+
# Check Python version
152+
if sys.version_info < (3, 9):
153+
print("This script requires Python 3.9 or later")
154+
exit(1)
155+
156+
app = ConfigEditorApp()
157+
158+
# List of tuples for the target choices containing the target name and if it is enabled by default
159+
target_choices = []
160+
161+
# Parse build JSON file
162+
build_json_path = os.path.join(app.ROOT_PATH, "configs", "builds.json")
163+
if os.path.isfile(build_json_path):
164+
with open(build_json_path, "r") as build_json_file:
165+
build_json = json.load(build_json_file)
166+
for target in build_json["targets"]:
167+
try:
168+
default = False if target["skip"] else True
169+
except:
170+
default = True
171+
target_choices.append((target["target"], default))
172+
else:
173+
print("Error: configs/builds.json file not found.")
174+
exit(1)
175+
176+
target_choices.sort(key=lambda x: x[0])
177+
178+
parser = argparse.ArgumentParser(description="Configure and compile the ESP32 Arduino static libraries")
179+
180+
parser.add_argument("-t", "--target",
181+
metavar="<target>",
182+
type=str,
183+
default="default",
184+
required=False,
185+
help="Comma-separated list of targets to be compiled. Choose from: " + ", ".join([x[0] for x in target_choices])
186+
+ ". Default: All except " + ", ".join([x[0] for x in target_choices if not x[1]]))
187+
188+
parser.add_argument("--copy",
189+
type=bool,
190+
action=argparse.BooleanOptionalAction,
191+
default=True,
192+
required=False,
193+
help="Enable/disable copying the compiled libraries to arduino-esp32. Enabled by default")
194+
195+
parser.add_argument("-c", "--arduino-path",
196+
metavar="<arduino path>",
197+
type=str,
198+
default=arduino_default_path(),
199+
required=False,
200+
help="Path to arduino-esp32 directory. Default: " + arduino_default_path())
201+
202+
parser.add_argument("-A", "--arduino-branch",
203+
metavar="<arduino branch>",
204+
type=str,
205+
default="",
206+
required=False,
207+
help="Branch of the arduino-esp32 repository to be used")
208+
209+
parser.add_argument("-I", "--idf-branch",
210+
metavar="<IDF branch>",
211+
type=str,
212+
default="",
213+
required=False,
214+
help="Branch of the ESP-IDF repository to be used")
215+
216+
parser.add_argument("-i", "--idf-commit",
217+
metavar="<IDF commit>",
218+
type=str,
219+
default="",
220+
required=False,
221+
help="Commit of the ESP-IDF repository to be used")
222+
223+
debug_level_choices = ("default", "none", "error", "warning", "info", "debug", "verbose")
224+
parser.add_argument("-D", "--debug-level",
225+
metavar="<level>",
226+
type=str,
227+
default="default",
228+
choices=debug_level_choices,
229+
required=False,
230+
help="Debug level to be set to ESP-IDF. Choose from: " + ", ".join(debug_level_choices))
231+
232+
args = parser.parse_args()
233+
234+
# Set the options in the app
235+
if args.target.strip() == "default":
236+
args.target = ",".join([x[0] for x in target_choices if x[1]])
237+
elif args.target.strip() == "all":
238+
args.target = ",".join([x[0] for x in target_choices])
239+
240+
app.supported_targets = [x[0] for x in target_choices]
241+
242+
for target in args.target.split(","):
243+
if target not in app.supported_targets:
244+
print("Invalid target: " + target)
245+
exit(1)
246+
247+
app.setting_target = args.target
248+
249+
if args.copy:
250+
if check_arduino_path(args.arduino_path):
251+
app.setting_enable_copy = True
252+
elif args.arduino_path == arduino_default_path():
253+
print("Warning: Default Arduino path not found. Disabling copy to Arduino.")
254+
app.setting_enable_copy = False
255+
else:
256+
print("Invalid path to Arduino core: " + os.path.abspath(args.arduino_path))
257+
exit(1)
258+
else:
259+
app.setting_enable_copy = False
260+
261+
app.setting_arduino_path = os.path.abspath(args.arduino_path)
262+
app.setting_arduino_branch = args.arduino_branch
263+
app.setting_idf_branch = args.idf_branch
264+
app.setting_idf_commit = args.idf_commit
265+
app.setting_debug_level = args.debug_level
266+
267+
# Change to the root directory of the app to the root of the project
268+
os.chdir(app.ROOT_PATH)
269+
270+
# Main function to run the app
271+
app.run()
272+
273+
# Propagate the exit code from the app
274+
exit(app.return_code or 0)
275+
276+
if __name__ == "__main__":
277+
# If this script is run directly, start the app
278+
main()

‎tools/config_editor/compile.py

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import sys
2+
import subprocess
3+
import os
4+
5+
from rich.console import RenderableType
6+
7+
from textual import on, work
8+
from textual.app import ComposeResult
9+
from textual.binding import Binding
10+
from textual.events import ScreenResume
11+
from textual.containers import Container
12+
from textual.screen import Screen
13+
from textual.widgets import Header, Static, RichLog, Button, Footer
14+
15+
class CompileScreen(Screen):
16+
# Compile screen
17+
18+
# Set the key bindings
19+
BINDINGS = [
20+
Binding("escape", "back", "Back")
21+
]
22+
23+
# Child process running the libraries compilation
24+
child_process = None
25+
26+
log_widget: RichLog
27+
button_widget: Button
28+
29+
def action_back(self) -> None:
30+
self.workers.cancel_all()
31+
if self.child_process:
32+
# Terminate the child process if it is running
33+
print("Terminating child process")
34+
self.child_process.terminate()
35+
try:
36+
self.child_process.stdout.close()
37+
self.child_process.stderr.close()
38+
except:
39+
pass
40+
self.child_process.wait()
41+
self.dismiss()
42+
43+
def print_output(self, renderable: RenderableType, style=None) -> None:
44+
# Print output to the RichLog widget
45+
if style is None:
46+
self.log_widget.write(renderable)
47+
else:
48+
# Check the available styles at https://rich.readthedocs.io/en/stable/style.html
49+
self.log_widget.write("[" + str(style) + "]" + renderable)
50+
51+
def print_error(self, error: str) -> None:
52+
# Print error to the RichLog widget
53+
self.log_widget.write("[b bright_red]" + error)
54+
self.button_widget.add_class("-error")
55+
#print("Error: " + error) # For debugging
56+
57+
def print_success(self, message: str) -> None:
58+
# Print success message to the RichLog widget
59+
self.log_widget.write("[b bright_green]" + message)
60+
self.button_widget.add_class("-success")
61+
#print("Success: " + message) # For debugging
62+
63+
def print_info(self, message: str) -> None:
64+
# Print info message to the RichLog widget
65+
self.log_widget.write("[b bright_cyan]" + message)
66+
#print("Info: " + message) # For debugging
67+
68+
@work(name="compliation_worker", group="compilation", exclusive=True, thread=True)
69+
def compile_libs(self) -> None:
70+
# Compile the libraries
71+
print("Starting compilation process")
72+
73+
label = self.query_one("#compile-title", Static)
74+
self.child_process = None
75+
if self.app.setting_target == ",".join(self.app.supported_targets):
76+
target = "all targets"
77+
else:
78+
target = self.app.setting_target.replace(",", ", ").upper()
79+
80+
label.update("Compiling for " + target)
81+
self.print_info("======== Compiling for " + target + " ========")
82+
83+
command = ["./build.sh", "-t", self.app.setting_target, "-D", self.app.setting_debug_level]
84+
85+
#command.append("--help") # For testing output without compiling
86+
87+
if self.app.setting_enable_copy:
88+
if os.path.isdir(self.app.setting_arduino_path):
89+
command.extend(["-c", self.app.setting_arduino_path])
90+
else:
91+
self.print_error("Invalid path to Arduino core: " + self.app.setting_arduino_path)
92+
label.update("Invalid path to Arduino core")
93+
return
94+
95+
if self.app.setting_arduino_branch:
96+
command.extend(["-A", self.app.setting_arduino_branch])
97+
98+
if self.app.setting_idf_branch:
99+
command.extend(["-I", self.app.setting_idf_branch])
100+
101+
if self.app.setting_idf_commit:
102+
command.extend(["-i", self.app.setting_idf_commit])
103+
104+
self.print_info("Running: " + " ".join(command) + "\n")
105+
self.child_process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
106+
try:
107+
for output in self.child_process.stdout:
108+
if output == '' and self.child_process.poll() is not None:
109+
break
110+
if output:
111+
self.print_output(output.strip()) # Update RichLog widget with subprocess output
112+
self.child_process.stdout.close()
113+
except Exception as e:
114+
print("Error reading child process output: " + str(e))
115+
print("Process might have terminated")
116+
117+
if not self.child_process:
118+
self.print_error("Compilation failed for " + target + "Child process failed to start")
119+
label.update("Compilation failed for " + target + "Child process failed to start")
120+
return
121+
else:
122+
self.child_process.wait()
123+
124+
if self.child_process.returncode != 0:
125+
self.print_error("Compilation failed for " + target + ". Return code: " + str(self.child_process.returncode))
126+
self.print_error("Errors:")
127+
try:
128+
for error in self.child_process.stderr:
129+
if error:
130+
self.print_error(error.strip())
131+
self.child_process.stderr.close()
132+
except Exception as e:
133+
print("Error reading child process errors: " + str(e))
134+
label.update("Compilation failed for " + target)
135+
else:
136+
self.print_success("Compilation successful for " + target)
137+
label.update("Compilation successful for " + target)
138+
139+
def on_button_pressed(self, event: Button.Pressed) -> None:
140+
# Event handler called when a button is pressed
141+
self.action_back()
142+
143+
@on(ScreenResume)
144+
def on_resume(self) -> None:
145+
# Event handler called every time the screen is activated
146+
print("Compile screen resumed. Clearing logs and starting compilation process")
147+
self.button_widget.remove_class("-error")
148+
self.button_widget.remove_class("-success")
149+
self.log_widget.clear()
150+
self.log_widget.focus()
151+
self.compile_libs()
152+
153+
def compose(self) -> ComposeResult:
154+
# Compose the compilation screen
155+
yield Header()
156+
with Container(id="compile-log-container"):
157+
self.log_widget = RichLog(markup=True, id="compile-log")
158+
yield self.log_widget
159+
with Container(id="compile-status-container"):
160+
yield Static("Compiling for ...", id="compile-title")
161+
self.button_widget = Button("Back", id="compile-back-button")
162+
yield self.button_widget
163+
yield Footer()

‎tools/config_editor/editor.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import os
2+
3+
from textual import on
4+
from textual.app import ComposeResult
5+
from textual.binding import Binding
6+
from textual.containers import Container, VerticalScroll, Horizontal
7+
from textual.screen import Screen
8+
from textual.events import ScreenResume
9+
from textual.widgets import DirectoryTree, Header, TextArea, Button, Footer
10+
11+
class EditorScreen(Screen):
12+
# Configuration file editor screen
13+
14+
# Set the key bindings
15+
BINDINGS = [
16+
Binding("ctrl+s", "save", "Save", priority=True),
17+
Binding("escape", "app.pop_screen", "Discard")
18+
]
19+
20+
# Current file being edited
21+
current_file = ""
22+
23+
def action_save(self) -> None:
24+
code_view = self.query_one("#code", TextArea)
25+
current_text = code_view.text
26+
try:
27+
file = open(self.curent_file, "w")
28+
file.write(current_text)
29+
file.close()
30+
except Exception:
31+
print("Error saving file: " + self.curent_file)
32+
self.sub_title = "ERROR"
33+
else:
34+
print("File saved: " + self.curent_file)
35+
self.sub_title = self.curent_file
36+
self.dismiss()
37+
38+
def on_button_pressed(self, event: Button.Pressed) -> None:
39+
# Event handler called when a button is pressed
40+
if event.button.id == "save-editor-button" and self.curent_file != "":
41+
print("Save button pressed. Trying to save file: " + self.curent_file)
42+
self.action_save()
43+
elif event.button.id == "cancel-editor-button":
44+
print("Cancel button pressed")
45+
self.dismiss()
46+
47+
def on_directory_tree_file_selected(self, event: DirectoryTree.FileSelected) -> None:
48+
# Called when the user click a file in the directory tree
49+
event.stop()
50+
code_view = self.query_one("#code", TextArea)
51+
code_view.clear()
52+
self.curent_file = str(event.path)
53+
try:
54+
print("Opening file: " + self.curent_file)
55+
file = open(self.curent_file, "r")
56+
file_content = file.read()
57+
file.close()
58+
except Exception:
59+
print("Error opening file: " + self.curent_file)
60+
self.sub_title = "ERROR"
61+
else:
62+
print("File opened: " + self.curent_file)
63+
code_view.insert(file_content)
64+
self.sub_title = self.curent_file
65+
66+
@on(ScreenResume)
67+
def on_resume(self) -> None:
68+
# Event handler called every time the screen is activated
69+
print("Editor screen resumed. Clearing code view")
70+
self.sub_title = "Select a file"
71+
self.query_one(DirectoryTree).focus()
72+
self.query_one(TextArea).clear()
73+
self.curent_file = ""
74+
75+
def compose(self) -> ComposeResult:
76+
# Compose editor screen
77+
path = os.path.join(self.app.ROOT_PATH, 'configs')
78+
yield Header()
79+
with Container():
80+
yield DirectoryTree(path, id="tree-view")
81+
with VerticalScroll(id="code-view"):
82+
yield TextArea.code_editor("", id="code")
83+
with Horizontal(id="editor-buttons-container"):
84+
yield Button("Save", id="save-editor-button", classes="editor-button")
85+
yield Button("Cancel", id="cancel-editor-button", classes="editor-button")
86+
yield Footer()

‎tools/config_editor/settings.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import math
2+
3+
from textual import on
4+
from textual.app import ComposeResult
5+
from textual.binding import Binding
6+
from textual.containers import VerticalScroll, Container, Horizontal
7+
from textual.screen import Screen
8+
from textual.events import ScreenResume
9+
from textual.widgets import Header, Button, Switch, Label, Footer, Checkbox
10+
11+
from widgets import LabelledInput, LabelledSelect
12+
13+
class SettingsScreen(Screen):
14+
# Settings screen
15+
16+
# Set the key bindings
17+
BINDINGS = [
18+
Binding("s", "save", "Save"),
19+
Binding("escape", "app.pop_screen", "Discard")
20+
]
21+
22+
enable_copy_switch: Switch
23+
arduino_path_input: LabelledInput
24+
arduino_branch_input: LabelledInput
25+
idf_branch_input: LabelledInput
26+
idf_commit_input: LabelledInput
27+
idf_debug_select: LabelledSelect
28+
29+
def action_save(self) -> None:
30+
checkboxes = self.query(Checkbox)
31+
self.app.setting_target = ""
32+
for checkbox in checkboxes:
33+
if checkbox.value:
34+
if self.app.setting_target:
35+
self.app.setting_target += ","
36+
self.app.setting_target += checkbox.id.replace("-checkbox", "")
37+
print("Target setting updated: " + self.app.setting_target)
38+
39+
self.app.setting_enable_copy = self.enable_copy_switch.value
40+
print("Enable copy setting updated: " + str(self.app.setting_enable_copy))
41+
42+
if self.enable_copy_switch.value:
43+
self.app.setting_arduino_path = self.arduino_path_input.get_input_value()
44+
print("Arduino path setting updated: " + self.app.setting_arduino_path)
45+
46+
self.app.setting_arduino_branch = self.arduino_branch_input.get_input_value()
47+
print("Arduino branch setting updated: " + self.app.setting_arduino_branch)
48+
49+
self.app.setting_idf_branch = self.idf_branch_input.get_input_value()
50+
print("IDF branch setting updated: " + self.app.setting_idf_branch)
51+
52+
self.app.setting_idf_commit = self.idf_commit_input.get_input_value()
53+
print("IDF commit setting updated: " + self.app.setting_idf_commit)
54+
55+
self.app.setting_debug_level = self.idf_debug_select.get_select_value()
56+
print("Debug level setting updated: " + self.app.setting_debug_level)
57+
58+
def on_button_pressed(self, event: Button.Pressed) -> None:
59+
# Event handler called when a button is pressed
60+
if event.button.id == "save-settings-button":
61+
print("Save button pressed")
62+
self.action_save()
63+
elif event.button.id == "cancel-settings-button":
64+
print("Cancel button pressed")
65+
self.dismiss()
66+
67+
@on(ScreenResume)
68+
def on_resume(self) -> None:
69+
# Event handler called every time the screen is activated
70+
print("Settings screen resumed. Updating settings.")
71+
targets = self.app.setting_target.split(",")
72+
checkboxes = self.query(Checkbox)
73+
for checkbox in checkboxes:
74+
checkbox.value = False
75+
if checkbox.id.replace("-checkbox", "") in targets:
76+
checkbox.value = True
77+
self.enable_copy_switch.value = self.app.setting_enable_copy
78+
if self.app.setting_enable_copy:
79+
self.arduino_path_input.visible = True
80+
else:
81+
self.arduino_path_input.visible = False
82+
self.arduino_path_input.set_input_value(self.app.setting_arduino_path)
83+
self.arduino_branch_input.set_input_value(self.app.setting_arduino_branch)
84+
self.idf_branch_input.set_input_value(self.app.setting_idf_branch)
85+
self.idf_commit_input.set_input_value(self.app.setting_idf_commit)
86+
self.idf_debug_select.set_select_value(self.app.setting_debug_level)
87+
88+
def on_switch_changed(self, event: Switch.Changed) -> None:
89+
# Event handler called when a switch is changed
90+
if event.switch.id == "enable-copy-switch":
91+
if event.switch.value:
92+
self.arduino_path_input.visible = True
93+
else:
94+
self.arduino_path_input.visible = False
95+
96+
def compose(self) -> ComposeResult:
97+
# Compose the target selection screen
98+
yield Header()
99+
with VerticalScroll(id="settings-scroll-container"):
100+
101+
yield Label("Compilation Targets", id="settings-target-label")
102+
with Container(id="settings-target-container"):
103+
for target in self.app.supported_targets:
104+
yield Checkbox(target.upper(), id=target + "-checkbox")
105+
106+
with Horizontal(classes="settings-switch-container"):
107+
self.enable_copy_switch = Switch(value=self.app.setting_enable_copy, id="enable-copy-switch")
108+
yield self.enable_copy_switch
109+
110+
yield Label("Copy to arduino-esp32 after compilation")
111+
112+
self.arduino_path_input = LabelledInput("Arduino-esp32 Path", placeholder="Path to your arduino-esp32 installation", value=self.app.setting_arduino_path, id="arduino-path-input")
113+
yield self.arduino_path_input
114+
115+
self.arduino_branch_input = LabelledInput("Arduino-esp32 Branch", placeholder="Leave empty to use default", value=self.app.setting_arduino_branch, id="arduino-branch-input")
116+
yield self.arduino_branch_input
117+
118+
self.idf_branch_input = LabelledInput("ESP-IDF Branch", placeholder="Leave empty to use default", value=self.app.setting_idf_branch, id="idf-branch-input")
119+
yield self.idf_branch_input
120+
121+
self.idf_commit_input = LabelledInput("ESP-IDF Commit", placeholder="Leave empty to use default", value=self.app.setting_idf_commit, id="idf-commit-input")
122+
yield self.idf_commit_input
123+
124+
debug_options = [
125+
("Default", "default"),
126+
("None", "none"),
127+
("Error", "error"),
128+
("Warning", "warning"),
129+
("Info", "info"),
130+
("Debug", "debug"),
131+
("Verbose", "verbose")
132+
]
133+
self.idf_debug_select = LabelledSelect("ESP-IDF Debug Level", debug_options, allow_blank=False, id="idf-debug-select")
134+
yield self.idf_debug_select
135+
136+
with Horizontal(id="settings-button-container"):
137+
yield Button("Save", id="save-settings-button", classes="settings-button")
138+
yield Button("Cancel", id="cancel-settings-button", classes="settings-button")
139+
yield Footer()
140+
141+
def on_mount(self) -> None:
142+
# Event handler called when the screen is mounted for the first time
143+
self.sub_title = "Settings"
144+
target_container = self.query_one("#settings-target-container")
145+
# Height needs to be 3 for each row of targets + 1
146+
height_value = str(int(math.ceil(len(self.app.supported_targets) / int(target_container.styles.grid_size_columns)) * 3 + 1))
147+
print("Target container height: " + height_value)
148+
target_container.styles.height = height_value
149+
print("Settings screen mounted")

‎tools/config_editor/style.tcss

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
# General
2+
3+
Screen {
4+
background: $surface-darken-1;
5+
}
6+
7+
Button {
8+
width: auto;
9+
min-width: 16;
10+
height: auto;
11+
color: $text;
12+
border: none;
13+
background: #038c8c;
14+
border-top: tall #026868;
15+
border-bottom: tall #6ab8b8;
16+
text-align: center;
17+
content-align: center middle;
18+
text-style: bold;
19+
20+
&:focus {
21+
text-style: bold reverse;
22+
}
23+
&:hover {
24+
border-top: tall #014444;
25+
border-bottom: tall #3d8080;
26+
background: #025b5b;
27+
color: $text;
28+
}
29+
&.-active {
30+
background: #025b5b;
31+
border-bottom: tall #3d8080;
32+
border-top: tall #014444;
33+
tint: $background 30%;
34+
}
35+
36+
&.-success {
37+
background: $success;
38+
color: $text;
39+
border-top: tall $success-lighten-2;
40+
border-bottom: tall $success-darken-3;
41+
42+
&:hover {
43+
background: $success-darken-2;
44+
color: $text;
45+
border-top: tall $success;
46+
}
47+
48+
&.-active {
49+
background: $success;
50+
border-bottom: tall $success-lighten-2;
51+
border-top: tall $success-darken-2;
52+
}
53+
}
54+
55+
&.-error {
56+
background: $error;
57+
color: $text;
58+
border-top: tall $error-lighten-2;
59+
border-bottom: tall $error-darken-3;
60+
61+
&:hover {
62+
background: $error-darken-1;
63+
color: $text;
64+
border-top: tall $error;
65+
}
66+
67+
&.-active {
68+
background: $error;
69+
border-bottom: tall $error-lighten-2;
70+
border-top: tall $error-darken-2;
71+
}
72+
}
73+
}
74+
75+
# Main Screen
76+
77+
.main-menu-button {
78+
margin-bottom: 1;
79+
min-width: 100%;
80+
max-width: 0.4fr;
81+
}
82+
83+
#main-menu-container {
84+
align: center middle;
85+
width: 1fr;
86+
}
87+
88+
#main-menu-title {
89+
text-align: center;
90+
margin-bottom: 4;
91+
text-style: bold;
92+
color: auto;
93+
width: 0.4fr;
94+
}
95+
96+
# Compile Screen
97+
98+
#compile-status-container {
99+
layout: horizontal;
100+
padding: 0 2;
101+
height: 4;
102+
}
103+
104+
#compile-title {
105+
dock: left;
106+
}
107+
108+
#compile-back-button {
109+
dock: right;
110+
}
111+
112+
#compile-log {
113+
background: $surface;
114+
padding: 0 1 1 1;
115+
margin: 1 2;
116+
}
117+
118+
# Settings Screen
119+
120+
#settings-scroll-container {
121+
padding: 1;
122+
}
123+
124+
#settings-button-container {
125+
width: 100%;
126+
max-height: 20%;
127+
min-height: 5;
128+
align: center middle;
129+
}
130+
131+
#settings-target-label {
132+
margin-left: 1;
133+
}
134+
135+
#settings-target-container {
136+
layout: grid;
137+
grid-size: 4;
138+
}
139+
140+
#settings-target-container Checkbox {
141+
width: 100%;
142+
margin-right: -1;
143+
}
144+
145+
.settings-button {
146+
margin: 1;
147+
min-width: 100%;
148+
max-width: 0.2fr;
149+
align: center middle;
150+
}
151+
152+
.settings-switch-container {
153+
height: 4;
154+
}
155+
156+
.settings-switch-container Switch {
157+
margin-right: 2;
158+
}
159+
160+
.settings-switch-container Label {
161+
margin-top: 1;
162+
}
163+
164+
# Editor Screen
165+
166+
#tree-view {
167+
display: none;
168+
scrollbar-gutter: stable;
169+
overflow: auto;
170+
width: auto;
171+
height: 100%;
172+
dock: left;
173+
display: block;
174+
max-width: 50%;
175+
}
176+
177+
#code-view {
178+
overflow: auto scroll;
179+
min-width: 100%;
180+
}
181+
182+
#code {
183+
width: 100%;
184+
}
185+
186+
.editor-button {
187+
width: 20%;
188+
}
189+
190+
#save-editor-button {
191+
dock: left;
192+
margin: 1;
193+
}
194+
195+
#cancel-editor-button {
196+
dock: right;
197+
margin: 1 3;
198+
}
199+
200+
#editor-buttons-container {
201+
height: 5;
202+
}

‎tools/config_editor/widgets.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
from textual.widget import Widget
2+
3+
from textual.widgets import Input, Label, Select
4+
5+
class LabelledInput(Widget):
6+
DEFAULT_CSS = """
7+
LabelledInput {
8+
height: 4;
9+
margin-bottom: 1;
10+
}
11+
LabelledInput Label {
12+
padding-left: 1;
13+
}
14+
"""
15+
16+
label_widget: Label
17+
input_widget: Input
18+
19+
def set_input_value(self, value):
20+
self.input_widget.value = value
21+
22+
def get_input_value(self):
23+
return self.input_widget.value
24+
25+
def __init__(self,
26+
label,
27+
*,
28+
placeholder="",
29+
value="",
30+
name=None,
31+
id=None,
32+
classes=None,
33+
disabled=False):
34+
super().__init__(name=name, id=id, classes=classes, disabled=disabled)
35+
self.__label = label
36+
self.__placeholder = placeholder
37+
self.__init_value = value
38+
39+
def compose(self):
40+
self.label_widget = Label(f"{self.__label}:")
41+
self.input_widget = Input(placeholder=self.__placeholder, value=self.__init_value)
42+
yield self.label_widget
43+
yield self.input_widget
44+
45+
46+
class LabelledSelect(Widget):
47+
DEFAULT_CSS = """
48+
LabelledSelect {
49+
height: 4;
50+
margin-bottom: 1;
51+
}
52+
LabelledSelect Label {
53+
padding-left: 1;
54+
}
55+
"""
56+
57+
label_widget: Label
58+
select_widget: Select
59+
60+
def set_select_options(self, options):
61+
self.__options = options
62+
self.select_widget.options = options
63+
64+
def get_select_options(self):
65+
return self.__options
66+
67+
def set_select_value(self, value):
68+
self.select_widget.value = value
69+
70+
def get_select_value(self):
71+
return self.select_widget.value
72+
73+
def __init__(self,
74+
label,
75+
options,
76+
*,
77+
prompt="Select",
78+
allow_blank=True,
79+
value=Select.BLANK,
80+
name=None,
81+
id=None,
82+
classes=None,
83+
disabled=False):
84+
super().__init__(name=name, id=id, classes=classes, disabled=disabled)
85+
self.__label = label
86+
self.__options = options
87+
self.__init_value = value
88+
self.__prompt = prompt
89+
self.__allow_blank = allow_blank
90+
91+
def compose(self):
92+
self.label_widget = Label(f"{self.__label}:")
93+
self.select_widget = Select(options=self.__options, value=self.__init_value, prompt=self.__prompt, allow_blank=self.__allow_blank)
94+
yield self.label_widget
95+
yield self.select_widget

0 commit comments

Comments
 (0)
Please sign in to comment.