Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Config TUI Tool #106

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ libclang = "*"
clang = "*"
langchain = "*"
langchain-openai = "*"
urwid = "*"

[dev-packages]
pylint = "*"
Expand Down
4 changes: 4 additions & 0 deletions esbmc-ai-config
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env sh

python -m esbmc_ai_config $@

1 change: 1 addition & 0 deletions esbmc_ai_config/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Author: Yiannis Charalambous
30 changes: 30 additions & 0 deletions esbmc_ai_config/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Author: Yiannis Charalambous

from urwid import MainLoop

from esbmc_ai_config.context_manager import ContextManager
from esbmc_ai_config.contexts.main_menu import MainMenu
from esbmc_ai_config.models.config_manager import ConfigManager


palette = [
("banner", "", "", "", "#ffa", "#60d"),
("streak", "", "", "", "g50", "#60a"),
("inside", "", "", "", "g38", "#808"),
("outside", "", "", "", "g27", "#a06"),
("bg", "", "", "", "g7", "#d06"),
]


def main() -> None:
# TODO Use argparse to take arguments for initial env and config location to start
# loading from.
ConfigManager.init()
top_ctx = MainMenu()
app: MainLoop = MainLoop(top_ctx.widget, palette=[("reversed", "standout", "")])
ContextManager.init(app, top_ctx)
app.run()


if __name__ == "__main__":
main()
27 changes: 27 additions & 0 deletions esbmc_ai_config/context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Author: Yiannis Charalambous

from abc import abstractmethod
from typing import Optional
import urwid


class Context(urwid.WidgetWrap):
def __init__(self, widget: Optional[urwid.Widget] = None) -> None:
super().__init__(widget if widget else self.build_ui())

@property
def widget(self) -> urwid.Widget:
assert isinstance(self._wrapped_widget, urwid.Widget)
return self._wrapped_widget

@widget.setter
def widget(self, value: urwid.Widget) -> None:
self._wrapped_widget = value

@abstractmethod
def build_ui(self) -> urwid.Widget:
"""Provides a method to build the interface, if not provided."""
raise NotImplementedError

def refresh_ui(self) -> None:
self.widget = self.build_ui()
33 changes: 33 additions & 0 deletions esbmc_ai_config/context_manager.py
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

@classmethod
def push_context(cls, ctx: Context) -> None:
cls.view_stack.append(ctx)
cls.app.widget = ctx

@classmethod
def pop_context(cls) -> Context:
cls.app.widget = cls.view_stack[-2]
return cls.view_stack.pop()

@classmethod
def get_context(cls) -> Context:
return cls.view_stack[-1]
7 changes: 7 additions & 0 deletions esbmc_ai_config/contexts/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Author: Yiannis Charalambous

from .base_menu import BaseMenu

__all__ = [
"BaseMenu",
]
67 changes: 67 additions & 0 deletions esbmc_ai_config/contexts/ai_config_menu.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Author: Yiannis Charalambous

from typing_extensions import override

import urwid

from esbmc_ai.ai_models import AIModels

from esbmc_ai_config.models.config_manager import ConfigManager
from esbmc_ai_config.context_manager import ContextManager
from esbmc_ai_config.context import Context
from esbmc_ai_config.contexts.base_menu import BaseMenu
from esbmc_ai_config.contexts.list_select_dialog import ListSelectDialog


class AIConfigMenu(BaseMenu):
def __init__(self) -> None:
super().__init__(title="AI Configuration", choices=self._get_menu_choices())

def _get_menu_choices(self) -> list[str | urwid.Widget]:
return [
urwid.AttrMap(
urwid.Button(
"AI Model",
on_press=self._open_ai_model_dialog,
),
None,
"reversed",
),
"Temperature",
"API Request Cooldown",
]

def _open_ai_model_dialog(self, button: urwid.Button) -> None:
# Get built-in AI models.
options: list[str] = [ai_model.value.name for ai_model in AIModels]
ai_model = ConfigManager.json_config.get_value("ai_model")
current_option: int = 0
if isinstance(ai_model, str):
current_option = options.index(ai_model)
# TODO Load custom AI models
dialog: Context = ListSelectDialog(
title="AI Model",
options=options,
initial_choice=current_option,
item_selected=lambda ai_model: ConfigManager.json_config.set_value(
ai_model, "ai_model"
),
)

ContextManager.push_context(dialog)

@override
def build_ui(self) -> urwid.Widget:
self.choices = self._get_menu_choices()
menu: urwid.Widget = super().build_ui()
overlay: urwid.Widget = urwid.Overlay(
urwid.LineBox(menu),
urwid.SolidFill("\N{MEDIUM SHADE}"),
align="center",
valign="middle",
width=("relative", 60),
height=("relative", 15),
min_width=20,
min_height=10,
)
return overlay
73 changes: 73 additions & 0 deletions esbmc_ai_config/contexts/base_menu.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Author: Yiannis Charalambous

from typing_extensions import override
import urwid
from urwid import Button, Text, connect_signal, AttrMap

from esbmc_ai_config.context import Context
from esbmc_ai_config.widgets.back_button import BackButton


class BaseMenu(Context):
def __init__(
self,
title: str,
choices: list[str | urwid.Widget],
back_choice: bool = True,
initial_choice: int = 0,
) -> None:
"""Creates a Menu context and displays it on the screen."""

assert initial_choice >= 0 and initial_choice < len(choices) + (
1 if back_choice else 0
), f"Initial choice index invalid: {initial_choice} / {len(choices)}"

self.choices: list[str | urwid.Widget] = choices.copy()
self.back_choice: bool = back_choice
self.title: str = title
# + 2 for divider and title.
self.initial_choice: int = initial_choice + 2

super().__init__()

def create_padding(self, menu: urwid.Widget) -> urwid.Padding:
return urwid.Padding(
menu,
left=2,
right=2,
)

@override
def build_ui(self) -> urwid.Widget:
choices: list[str | urwid.Widget] = self.choices.copy()
if self.back_choice:
choices.append(urwid.Divider())
choices.append(BackButton())

menu: urwid.ListBox = self.create_menu(title=self.title, choices=choices)
return self.create_padding(menu)

def item_chosen(self, button, choice) -> None:
pass

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")

walker: urwid.SimpleFocusListWalker = urwid.SimpleFocusListWalker(body)
walker.focus = self.initial_choice
return urwid.ListBox(walker)
44 changes: 44 additions & 0 deletions esbmc_ai_config/contexts/dialog_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Author: Yiannis Charalambous

from typing_extensions import override

import urwid
from esbmc_ai_config.context import Context
from esbmc_ai_config.context_manager import ContextManager


class DialogContext(Context):
def __init__(self, title: str = "", message: str = "") -> None:
self.title: str = title
self.message: str = message

super().__init__()

def _on_ok(self, button) -> None:
ContextManager.pop_context()

@override
def build_ui(self) -> urwid.Widget:
ok_button: urwid.Button = urwid.Button("OK", on_press=self._on_ok)

body: list[urwid.Widget] = [
urwid.Text(self.title),
urwid.Divider(),
urwid.Text(self.message),
urwid.Divider(),
urwid.Columns([urwid.AttrMap(ok_button, None, focus_map="reversed")]),
]

list_menu: urwid.ListBox = urwid.ListBox(body)
# Wrap in nice UI.
overlay: urwid.Widget = urwid.Overlay(
urwid.LineBox(urwid.Padding(list_menu, left=2, right=2)),
urwid.SolidFill("\N{MEDIUM SHADE}"),
align="center",
valign="middle",
width=("relative", 60),
height=("relative", 15),
min_width=20,
min_height=10,
)
return overlay
62 changes: 62 additions & 0 deletions esbmc_ai_config/contexts/env_menu.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Author: Yiannis Charalambous

from typing_extensions import override
import urwid
from esbmc_ai_config.models.config_manager import ConfigManager

from esbmc_ai_config.models import EnvConfigField
from esbmc_ai_config.contexts.base_menu import BaseMenu
from esbmc_ai_config.widgets.text_input_button import TextInputButton


class EnvMenu(BaseMenu):
def __init__(self) -> None:
super().__init__(title="Setup Environment", choices=self._get_menu_items())

def _get_menu_items(self) -> list[str | urwid.Widget]:
choices: list[str | urwid.Widget] = [
TextInputButton(
field.name,
on_submit=self._on_value_submit,
initial_value=str(ConfigManager.env_config.values[field.name]),
)
for field in ConfigManager.env_config.fields
if field.show_in_config
]
return choices

@override
def build_ui(self) -> urwid.Widget:
# Update value of choices to contain new objects of TextInputButton
# with initial values.
self.choices: list[str | urwid.Widget] = self._get_menu_items()
# Build the BaseMenu UI.
menu: urwid.Widget = super().build_ui()
# Wrap in nice UI.
overlay: urwid.Widget = urwid.Overlay(
urwid.LineBox(menu),
urwid.SolidFill("\N{MEDIUM SHADE}"),
align="center",
valign="middle",
width=("relative", 60),
height=("relative", 15),
min_width=20,
min_height=10,
)

return overlay

def _on_value_submit(self, title: str, value: str, ok_pressed: bool) -> None:
if ok_pressed:
# Find the correct field.
field: EnvConfigField
for i in ConfigManager.env_config.fields:
if i.name == title:
field = i
break
else:
return

# Cast to the correct type.
ConfigManager.env_config.values[title] = type(field.default_value)(value)
self.refresh_ui()
5 changes: 5 additions & 0 deletions esbmc_ai_config/contexts/esbmc_menu/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Author: Yiannis Charalambous

from .esbmc_menu import ESBMCMenu

__all__ = ["ESBMCMenu"]
Loading
Loading