-
-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
1710e10
commit fcf5168
Showing
12 changed files
with
377 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
#!/usr/bin/env sh | ||
|
||
python -m esbmc_ai_config $@ | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
# Author: Yiannis Charalambous |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
# Author: Yiannis Charalambous | ||
|
||
from urwid import ( | ||
MainLoop, | ||
) | ||
|
||
from esbmc_ai_config.context_manager import ContextManager | ||
from esbmc_ai_config.contexts.main_menu import MainMenu | ||
|
||
|
||
palette = [ | ||
("banner", "", "", "", "#ffa", "#60d"), | ||
("streak", "", "", "", "g50", "#60a"), | ||
("inside", "", "", "", "g38", "#808"), | ||
("outside", "", "", "", "g27", "#a06"), | ||
("bg", "", "", "", "g7", "#d06"), | ||
] | ||
|
||
|
||
def main() -> None: | ||
top_ctx = MainMenu() | ||
|
||
app: MainLoop = MainLoop(top_ctx.widget, palette=[("reversed", "standout", "")]) | ||
ContextManager.init(app, top_ctx) | ||
app.run() | ||
|
||
|
||
if __name__ == "__main__": | ||
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
# Author: Yiannis Charalambous | ||
|
||
import urwid | ||
|
||
|
||
class Context(object): | ||
def __init__(self, widget: urwid.Widget) -> None: | ||
super().__init__() | ||
|
||
self.widget: urwid.Widget = widget |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
# Author: Yiannis Charalambous | ||
|
||
from urwid import MainLoop | ||
|
||
from esbmc_ai_config.context import Context | ||
|
||
|
||
class ContextManager(object): | ||
app: MainLoop | ||
view_stack: list[Context] = [] | ||
|
||
def __init__(self) -> None: | ||
raise Exception("Static class cannot be instantiated...") | ||
|
||
@classmethod | ||
def init(cls, app: MainLoop, ctx: Context) -> None: | ||
cls.app = app | ||
cls.view_stack.append(ctx) | ||
cls.app.widget = ctx.widget | ||
|
||
@classmethod | ||
def push_context(cls, ctx: Context) -> None: | ||
cls.view_stack.append(ctx) | ||
cls.app.widget = ctx.widget | ||
|
||
@classmethod | ||
def pop_context(cls) -> Context: | ||
cls.app.widget = cls.view_stack[-2].widget | ||
return cls.view_stack.pop() | ||
|
||
@classmethod | ||
def get_context(cls) -> Context: | ||
return cls.view_stack[-1] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
# Author: Yiannis Charalambous | ||
|
||
import urwid | ||
from urwid import Button, Text, connect_signal, AttrMap | ||
|
||
from esbmc_ai_config.context import Context | ||
from esbmc_ai_config.context_manager import ContextManager | ||
|
||
|
||
class BaseMenu(Context): | ||
def __init__( | ||
self, | ||
title: str, | ||
choices: list[str | urwid.Widget], | ||
back_choice: bool = True, | ||
) -> None: | ||
"""Creates a Menu context and displays it on the screen.""" | ||
|
||
_choices: list[str | urwid.Widget] = choices.copy() | ||
if back_choice: | ||
_choices.append(urwid.Divider()) | ||
_choices.append("Back") | ||
|
||
menu: urwid.ListBox = self.create_menu(title=title, choices=_choices) | ||
|
||
menu_box: urwid.WidgetDecoration = urwid.Padding( | ||
menu, | ||
left=2, | ||
right=2, | ||
) | ||
|
||
super().__init__(menu_box) | ||
|
||
def item_chosen(self, button, choice) -> None: | ||
if choice == "Back": | ||
ContextManager.pop_context() | ||
|
||
def create_menu(self, title, choices: list[str | urwid.Widget]) -> urwid.ListBox: | ||
body: list[urwid.Widget] = [Text(title), urwid.Divider()] | ||
for c in choices: | ||
if isinstance(c, str): | ||
button = Button(c) | ||
connect_signal( | ||
obj=button, | ||
name="click", | ||
callback=self.item_chosen, | ||
user_arg=c, | ||
) | ||
# Reverse attributes when focused | ||
body.append(AttrMap(button, None, focus_map="reversed")) | ||
elif isinstance(c, urwid.Widget): | ||
body.append(c) | ||
else: | ||
raise ValueError(f"create_menu: {c} is not a str or urwid.Widget") | ||
|
||
return urwid.ListBox(urwid.SimpleFocusListWalker(body)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
# Author: Yiannis Charalambous | ||
|
||
import urwid | ||
|
||
from esbmc_ai_config.models.env_config_loader import EnvConfigLoader | ||
from esbmc_ai_config.contexts.base_menu import BaseMenu | ||
|
||
|
||
class EnvMenu(BaseMenu): | ||
def __init__(self) -> None: | ||
self.config: EnvConfigLoader = EnvConfigLoader(create_missing_fields=True) | ||
choices: list = [field.name for field in self.config.fields] | ||
super().__init__(title="Setup Environment", choices=choices) | ||
|
||
top: urwid.Widget = urwid.Overlay( | ||
urwid.LineBox(self.widget), | ||
urwid.SolidFill("\N{MEDIUM SHADE}"), | ||
align="center", | ||
valign="middle", | ||
width=("relative", 60), | ||
height=("relative", 15), | ||
min_width=20, | ||
min_height=10, | ||
) | ||
|
||
self.widget = top |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
# Author: Yiannis Charalambous | ||
|
||
|
||
from typing_extensions import override | ||
import urwid | ||
from urwid import Text, Widget | ||
|
||
from esbmc_ai_config.contexts.base_menu import BaseMenu | ||
from esbmc_ai_config.context_manager import ContextManager | ||
from esbmc_ai_config.contexts.env_menu import EnvMenu | ||
|
||
|
||
class MainMenu(BaseMenu): | ||
def __init__(self) -> None: | ||
super().__init__( | ||
title="Main Menu", | ||
choices=[ | ||
"Setup Environment", | ||
"ESBMC Settings", | ||
"AI Configuration", | ||
"Save", | ||
"Save As", | ||
urwid.Divider(), | ||
"Exit", | ||
], | ||
back_choice=False, | ||
) | ||
|
||
# Add additional content to widget. | ||
text = urwid.ListBox( | ||
[ | ||
Text( | ||
"ESBMC-AI CLI Configuration Tool\n" | ||
"Made by Yiannis Charalambous\n\n" | ||
"This tool configures ESBMC-AI through CLI menus using the ncurses library." | ||
"Not all options may be available, so it is worth checking the documentation:\n\n" | ||
"https://github.com/Yiannis128/esbmc-ai/wiki/Configuration\n\n" | ||
"Env File: ~/.config/esbmc-ai.env\n" | ||
"Config File: ~/.config/esbmc-ai.json\n\n" | ||
"Control Keys:\n" | ||
"- Up/Down: Navigation\n" | ||
"- Enter: Select Option\n" | ||
) | ||
] | ||
) | ||
|
||
top: Widget = urwid.Overlay( | ||
urwid.LineBox(self.widget), | ||
text, | ||
align="center", | ||
valign="middle", | ||
width=("relative", 60), | ||
height=("relative", 15), | ||
min_width=20, | ||
min_height=12, | ||
) | ||
|
||
self.widget = top | ||
|
||
@override | ||
def item_chosen(self, button, choice) -> None: | ||
super().item_chosen(button, choice) | ||
|
||
match choice: | ||
case "Exit": | ||
self.exit_program(button) | ||
case "Setup Environment": | ||
ContextManager.push_context(EnvMenu()) | ||
return | ||
|
||
def exit_program(self, _): | ||
raise urwid.ExitMainLoop() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
# Author: Yiannis Charalambous | ||
|
||
|
||
from abc import abstractmethod | ||
import os | ||
|
||
|
||
class ConfigLoader(object): | ||
"""Class responsible for loading the ESBMC-AI config. Is generic so contains | ||
methods for EnvConfigLoader and JsonConfigLoader""" | ||
|
||
def __init__(self, file_path: str, create_missing_fields: bool = False) -> None: | ||
self.file_path: str = os.path.expanduser(os.path.expandvars(file_path)) | ||
if os.path.exists(self.file_path) and os.path.isfile(self.file_path): | ||
with open(self.file_path, "r") as file: | ||
self.content: str = file.read() | ||
else: | ||
# Create default file. | ||
self._create_default_file() | ||
|
||
# Read fields. | ||
self._read_fields(create_missing_fields=create_missing_fields) | ||
|
||
@abstractmethod | ||
def _create_default_file(self) -> None: | ||
raise NotImplementedError() | ||
|
||
@abstractmethod | ||
def _read_fields(self, create_missing_fields: bool = False) -> None: | ||
raise NotImplementedError() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
# Author: Yiannis Charalambous | ||
|
||
import os | ||
import sys | ||
from dotenv import load_dotenv, find_dotenv | ||
from pathlib import Path | ||
from dataclasses import dataclass | ||
from typing_extensions import override, Optional | ||
from platform import system as system_name | ||
from esbmc_ai_config.models.config_loader import ConfigLoader | ||
|
||
__ALLOWED_ENV_TYPES = bool | float | int | str | ||
|
||
|
||
@dataclass | ||
class EnvConfigField: | ||
name: str | ||
default_value: "__ALLOWED_ENV_TYPES" = "" | ||
is_optional: bool = False | ||
"""Will not load the config if the value is not specified by the user. If | ||
true will assign default_value.""" | ||
|
||
|
||
class EnvConfigLoader(ConfigLoader): | ||
def __init__( | ||
self, | ||
file_path: str = "~/.config/esbmc-ai.env", | ||
fields: list[EnvConfigField] = [ | ||
EnvConfigField("ESBMC_AI_CFG_PATH", "", is_optional=False), | ||
EnvConfigField("OPENAI_API_KEY", "", is_optional=True), | ||
EnvConfigField("HUGGINGFACE_API_KEY", "", is_optional=True), | ||
], | ||
create_missing_fields: bool = False, | ||
) -> None: | ||
assert file_path.endswith(".env"), f"{self.file_path} is not a valid env file." | ||
|
||
self.fields: list[EnvConfigField] = fields | ||
|
||
self.values: dict[str, "__ALLOWED_ENV_TYPES"] = {} | ||
|
||
super().__init__( | ||
file_path=file_path, | ||
create_missing_fields=create_missing_fields, | ||
) | ||
|
||
@override | ||
def _create_default_file(self) -> None: | ||
with open(self.file_path, "w") as file: | ||
file.write("Generated by ESBMC-AI config tool.") | ||
|
||
@override | ||
def _read_fields(self, create_missing_fields: bool = False) -> None: | ||
"""Environment variables are loaded in the following order: | ||
1. Environment variables already loaded. Any variable not present will be looked for in | ||
.env files in the following locations. | ||
2. .env file in the current directory, moving upwards in the directory tree. | ||
3. esbmc-ai.env file in the current directory, moving upwards in the directory tree. | ||
4. esbmc-ai.env file in $HOME/.config/ for Linux/macOS and %userprofile% for Windows. | ||
Note: ESBMC_AI_CFG_PATH undergoes tilde user expansion and also environment | ||
variable expansion. | ||
""" | ||
|
||
values: dict[str, "__ALLOWED_ENV_TYPES"] = {} | ||
|
||
def get_env_vars() -> None: | ||
"""Gets all the system environment variables that are currently loaded. Will not | ||
load values that are not following the EnvConfigField specification.""" | ||
for field in self.fields: | ||
value: Optional[str] = os.getenv(field.name) | ||
# Check if value is not None | ||
if value != None: | ||
values[field.name] = value | ||
|
||
# Read from system environment. | ||
get_env_vars() | ||
|
||
# Search for .env or esbmc-ai.env in cwd and go up. | ||
# Find .env in file path and load it else find esbmc-ai.env in current working directory and load it. | ||
dotenv_file_path: str = find_dotenv(usecwd=True) | ||
if dotenv_file_path != "": | ||
load_dotenv(dotenv_path=dotenv_file_path, override=False, verbose=True) | ||
else: | ||
dotenv_file_path: str = find_dotenv(filename="esbmc-ai.env", usecwd=True) | ||
if dotenv_file_path != "": | ||
load_dotenv(dotenv_path=dotenv_file_path, override=False, verbose=True) | ||
|
||
get_env_vars() | ||
|
||
# Look for .env in home folder. | ||
home_path: Path = Path.home() | ||
match system_name(): | ||
case "Linux" | "Darwin": | ||
home_path /= ".config/esbmc-ai.env" | ||
case "Windows": | ||
home_path /= "esbmc-ai.env" | ||
case _: | ||
raise ValueError(f"Unknown OS type: {system_name()}") | ||
|
||
load_dotenv(home_path, override=False, verbose=True) | ||
get_env_vars() | ||
|
||
# Check if all the values are set, else create them with defaults. | ||
for field in self.fields: | ||
if field.name not in values: | ||
if create_missing_fields or field.is_optional: | ||
# Create new field with default value. | ||
values[field.name] = field.default_value | ||
else: | ||
print(f"Error: No ${field.name} in environment.") | ||
sys.exit(1) | ||
|
||
self.values: dict[str, "__ALLOWED_ENV_TYPES"] = values |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
# Author: Yiannis Charalambous |