diff --git a/pyproject.toml b/pyproject.toml index c13dec91..c5148cfa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "biscuit" -version = "2.95.0" +version = "2.96.0" description = "The uncompromising code editor" authors = ["Billy "] license = "MIT" diff --git a/src/__init__.py b/src/__init__.py index 3b65f1c5..b2ca67d5 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,4 +1,4 @@ -__version__ = "2.95.0" +__version__ = "2.96.0" __version_info__ = tuple([int(num) for num in __version__.split(".")]) import sys diff --git a/src/biscuit/common/actionset.py b/src/biscuit/common/actionset.py index c6551801..d51f8679 100644 --- a/src/biscuit/common/actionset.py +++ b/src/biscuit/common/actionset.py @@ -15,8 +15,8 @@ def __init__( self, description: str, prefix: str, - items: List[Tuple[str, Callable]] = [], - pinned: List[Tuple[str, Callable[[str], None]]] = [], + items: List[List[str | Callable]] = [], + pinned: List[List[str | Callable[[str], None]]] = [], *args, **kwargs ) -> None: @@ -25,8 +25,8 @@ def __init__( Args: description (str): The description of the actionset. prefix (str): The prefix of the actionset. - items (List[Tuple[str, Callable]], optional): The list of actions. Defaults to []. - pinned (List[Tuple[str, Callable[[str], None]]], optional): The list of pinned actions. Defaults to []. + items (List[Tuple[str | Callable]], optional): The list of actions. Defaults to []. + pinned (List[List[str | Callable[[str], None]]], optional): The list of pinned actions. Defaults to []. """ super().__init__(items, *args, **kwargs) diff --git a/src/biscuit/layout/drawer/drawer.py b/src/biscuit/layout/drawer/drawer.py index 6793d2bd..0e7583cd 100644 --- a/src/biscuit/layout/drawer/drawer.py +++ b/src/biscuit/layout/drawer/drawer.py @@ -120,7 +120,7 @@ def github(self) -> GitHub: def extensions(self) -> Extensions: return self.default_views["Extensions"] - def show_view(self, view: NavigationDrawerView) -> None: + def show_view(self, view: NavigationDrawerView) -> NavigationDrawerView: """Show a view in the drawer. Args: @@ -130,31 +130,31 @@ def show_view(self, view: NavigationDrawerView) -> None: if i.view == view: self.activitybar.set_active_slot(i) i.enable() - break + return view - def show_explorer(self) -> None: - self.show_view(self.explorer) + def show_explorer(self) -> Explorer: + return self.show_view(self.explorer) - def show_outline(self) -> None: - self.show_view(self.outline) + def show_outline(self) -> Outline: + return self.show_view(self.outline) - def show_search(self) -> None: - self.show_view(self.search) + def show_search(self) -> Search: + return self.show_view(self.search) - def show_source_control(self) -> None: - self.show_view(self.source_control) + def show_source_control(self) -> SourceControl: + return self.show_view(self.source_control) - def show_debug(self) -> None: - self.show_view(self.debug) + def show_debug(self) -> Debug: + return self.show_view(self.debug) - def show_ai(self) -> None: - self.show_view(self.ai) + def show_ai(self) -> AI: + return self.show_view(self.ai) - def show_github(self) -> None: - self.show_view(self.github) + def show_github(self) -> GitHub: + return self.show_view(self.github) - def show_extensions(self) -> None: - self.show_view(self.extensions) + def show_extensions(self) -> Extensions: + return self.show_view(self.extensions) def pack(self): super().pack(side=tk.LEFT, fill=tk.Y) diff --git a/src/biscuit/layout/panel/panelbar.py b/src/biscuit/layout/panel/panelbar.py index 3f79fb42..ceed5ca7 100644 --- a/src/biscuit/layout/panel/panelbar.py +++ b/src/biscuit/layout/panel/panelbar.py @@ -33,18 +33,18 @@ def __init__(self, master: Panel, *args, **kwargs) -> None: self.active_tabs: list[Tab] = [] self.active_tab = None - self.buttons: list[IconButton] = [] + self.actions: list[IconButton] = [] # These buttons are common for all panel views - self.default_buttons = ( + self.default_actions = ( ("close", content.toggle_panel), ("chevron-up", content.toggle_max_panel, "chevron-down"), ) - for button in self.default_buttons: + for button in self.default_actions: IconButton(self, *button).pack(side=tk.RIGHT) - def add_buttons(self, buttons: list[IconButton]) -> None: + def add_actions(self, buttons: list[IconButton]) -> None: """Add the buttons to the control buttons Args: @@ -52,21 +52,21 @@ def add_buttons(self, buttons: list[IconButton]) -> None: for button in buttons: button.pack(side=tk.LEFT) - self.buttons.append(button) + self.actions.append(button) - def replace_buttons(self, buttons: list[IconButton]) -> None: + def replace_actions(self, buttons: list[IconButton]) -> None: """Replace the control buttons with the new buttons Args: buttons (list[IconButton]): new buttons""" self.clear() - self.add_buttons(buttons) + self.add_actions(buttons) def clear(self) -> None: - for button in self.buttons: + for button in self.actions: button.pack_forget() - self.buttons.clear() + self.actions.clear() def add_tab(self, view: PanelView) -> None: """Add the view to the tabs @@ -87,7 +87,7 @@ def set_active_tab(self, selected_tab: Tab) -> None: selected_tab (Tab): selected tab""" self.active_tab = selected_tab - self.replace_buttons(selected_tab.view.__actions__) + self.replace_actions(selected_tab.view.__actions__) for tab in self.active_tabs: if tab != selected_tab: tab.deselect() diff --git a/src/biscuit/views/ai/ai.py b/src/biscuit/views/ai/ai.py index 8584d1fe..5d29776f 100644 --- a/src/biscuit/views/ai/ai.py +++ b/src/biscuit/views/ai/ai.py @@ -21,7 +21,7 @@ class AI(NavigationDrawerView): - The chat can be refreshed to start a new chat.""" def __init__(self, master, *args, **kwargs) -> None: - self.__buttons__ = [ + self.__actions__ = [ ("refresh", self.new_chat), ] super().__init__(master, *args, **kwargs) diff --git a/src/biscuit/views/debug/callstack.py b/src/biscuit/views/debug/callstack.py index 15811431..a0864189 100644 --- a/src/biscuit/views/debug/callstack.py +++ b/src/biscuit/views/debug/callstack.py @@ -11,7 +11,7 @@ class CallStack(NavigationDrawerViewItem): def __init__(self, master, *args, **kwargs) -> None: self.title = "Callstack" - self.__buttons__ = () + self.__actions__ = () super().__init__(master, itembar=True, *args, **kwargs) self.tree = Tree(self.content, cursor="hand2") diff --git a/src/biscuit/views/debug/debug.py b/src/biscuit/views/debug/debug.py index 13703302..78455836 100644 --- a/src/biscuit/views/debug/debug.py +++ b/src/biscuit/views/debug/debug.py @@ -18,7 +18,7 @@ class Debug(NavigationDrawerView): - Debugger run controls are displayed in the editor toolbar.""" def __init__(self, master, *args, **kwargs) -> None: - self.__buttons__ = [] + self.__actions__ = [] super().__init__(master, *args, **kwargs) self.__icon__ = "bug" self.name = "Debug" diff --git a/src/biscuit/views/debug/variables.py b/src/biscuit/views/debug/variables.py index bf15ba8c..015f5b9e 100644 --- a/src/biscuit/views/debug/variables.py +++ b/src/biscuit/views/debug/variables.py @@ -11,7 +11,7 @@ class Variables(NavigationDrawerViewItem): def __init__(self, master, *args, **kwargs) -> None: self.title = "Variables" - self.__buttons__ = () + self.__actions__ = () super().__init__(master, itembar=True, *args, **kwargs) self.tree = Tree(self.content, *args, **kwargs) diff --git a/src/biscuit/views/drawer_item.py b/src/biscuit/views/drawer_item.py index 2b821dfe..a09bbd1b 100644 --- a/src/biscuit/views/drawer_item.py +++ b/src/biscuit/views/drawer_item.py @@ -11,7 +11,7 @@ class NavigationDrawerViewItem(Frame): - Contains the itembar and the content of the item. """ - __buttons__ = [] + __actions__ = [] title = "Item" def __init__( @@ -38,7 +38,7 @@ def __init__( if itembar: self.itembar = NavigationDrawerItemToolBar( - self, self.title, self.__buttons__ + self, self.title, self.__actions__ ) self.itembar.grid(row=0, column=0, sticky=tk.NSEW) @@ -48,6 +48,10 @@ def __init__( self.content.grid_columnconfigure(0, weight=1) self.content.grid(row=1 if itembar else 0, column=0, sticky=tk.NSEW) + def add_action(self, icon: str, event) -> None: + if self.itembar_enabled: + self.itembar.add_action(icon, event) + def set_title(self, title: str) -> None: if self.itembar_enabled: self.itembar.set_title(title) diff --git a/src/biscuit/views/explorer/directorytree.py b/src/biscuit/views/explorer/directorytree.py index 36385729..95ccbd50 100644 --- a/src/biscuit/views/explorer/directorytree.py +++ b/src/biscuit/views/explorer/directorytree.py @@ -31,7 +31,7 @@ def __init__( **kwargs, ) -> None: self.title = "No folder opened" - self.__buttons__ = ( + self.__actions__ = ( ("new-file", lambda: self.base.palette.show("newfile:")), ("new-folder", lambda: self.base.palette.show("newfolder:")), ("refresh", self.refresh_root), diff --git a/src/biscuit/views/explorer/explorer.py b/src/biscuit/views/explorer/explorer.py index e25c28f8..cbd0d2a8 100644 --- a/src/biscuit/views/explorer/explorer.py +++ b/src/biscuit/views/explorer/explorer.py @@ -18,7 +18,7 @@ class Explorer(NavigationDrawerView): """ def __init__(self, master, *args, **kwargs) -> None: - self.__buttons__ = [] + self.__actions__ = [] super().__init__(master, *args, **kwargs) self.__icon__ = "files" self.name = "Explorer" diff --git a/src/biscuit/views/explorer/open_editors.py b/src/biscuit/views/explorer/open_editors.py index 872e21a4..ede46869 100644 --- a/src/biscuit/views/explorer/open_editors.py +++ b/src/biscuit/views/explorer/open_editors.py @@ -13,7 +13,7 @@ class OpenEditors(NavigationDrawerViewItem): def __init__(self, master, startpath=None, itembar=True, *args, **kwargs) -> None: self.title = "Open Editors" - self.__buttons__ = (("new-file", lambda: self.base.palette.show("newfile:")),) + self.__actions__ = (("new-file", lambda: self.base.palette.show("newfile:")),) super().__init__(master, itembar=itembar, *args, **kwargs) self.path = startpath self.nodes = {} diff --git a/src/biscuit/views/extensions/results.py b/src/biscuit/views/extensions/results.py index 9693170d..86399f1b 100644 --- a/src/biscuit/views/extensions/results.py +++ b/src/biscuit/views/extensions/results.py @@ -39,7 +39,7 @@ class Results(NavigationDrawerViewItem): fetching: threading.Event def __init__(self, master, *args, **kwargs) -> None: - self.__buttons__ = () + self.__actions__ = () self.title = "Available" super().__init__(master, *args, **kwargs) diff --git a/src/biscuit/views/github/github.py b/src/biscuit/views/github/github.py index f696e6d2..f615f5b9 100644 --- a/src/biscuit/views/github/github.py +++ b/src/biscuit/views/github/github.py @@ -16,7 +16,7 @@ class GitHub(NavigationDrawerView): """ def __init__(self, master, *args, **kwargs) -> None: - self.__buttons__ = [("refresh", self.on_directory_change)] + self.__actions__ = [("refresh", self.on_directory_change)] super().__init__(master, *args, **kwargs) self.__icon__ = "github" self.name = "GitHub" diff --git a/src/biscuit/views/github/issues.py b/src/biscuit/views/github/issues.py index 5a59e917..626d316b 100644 --- a/src/biscuit/views/github/issues.py +++ b/src/biscuit/views/github/issues.py @@ -22,7 +22,7 @@ class Issues(NavigationDrawerViewItem): def __init__(self, master, itembar=True, *args, **kwargs) -> None: self.title = "Open Issues" - self.__buttons__ = () + self.__actions__ = () super().__init__(master, itembar=itembar, *args, **kwargs) self.url_template = "https://api.github.com/repos/{}/{}/issues" diff --git a/src/biscuit/views/github/prs.py b/src/biscuit/views/github/prs.py index a504c8fa..bf391916 100644 --- a/src/biscuit/views/github/prs.py +++ b/src/biscuit/views/github/prs.py @@ -22,7 +22,7 @@ class PRs(NavigationDrawerViewItem): def __init__(self, master, itembar=True, *args, **kwargs) -> None: self.title = "Pull Requests" - self.__buttons__ = () + self.__actions__ = () super().__init__(master, itembar=itembar, *args, **kwargs) self.url_template = "https://api.github.com/repos/{}/{}/pulls" diff --git a/src/biscuit/views/logs/logs.py b/src/biscuit/views/logs/logs.py index 8b67b221..287ac6ca 100644 --- a/src/biscuit/views/logs/logs.py +++ b/src/biscuit/views/logs/logs.py @@ -44,7 +44,7 @@ class Logs(PanelView): def __init__(self, master, *args, **kwargs) -> None: super().__init__(master, *args, **kwargs) - self.__buttons__ = (("clear-all",),) + self.__actions__ = (("clear-all",),) self.rowconfigure(0, weight=1) self.columnconfigure(0, weight=1) diff --git a/src/biscuit/views/outline/outline.py b/src/biscuit/views/outline/outline.py index 7fa7abc0..628cc939 100644 --- a/src/biscuit/views/outline/outline.py +++ b/src/biscuit/views/outline/outline.py @@ -21,7 +21,7 @@ class Outline(NavigationDrawerView): """ def __init__(self, master, *args, **kwargs) -> None: - self.__buttons__ = [ + self.__actions__ = [ ("refresh",), ("collapse-all",), ("ellipsis",), diff --git a/src/biscuit/views/problems/problems.py b/src/biscuit/views/problems/problems.py index dba74615..1157318d 100644 --- a/src/biscuit/views/problems/problems.py +++ b/src/biscuit/views/problems/problems.py @@ -15,7 +15,7 @@ class Problems(PanelView): def __init__(self, master, *args, **kwargs) -> None: super().__init__(master, *args, **kwargs) - self.__buttons__ = (("clear-all", self.clear),) + self.__actions__ = (("clear-all", self.clear),) self.rowconfigure(0, weight=1) self.columnconfigure(0, weight=1) diff --git a/src/biscuit/views/search/search.py b/src/biscuit/views/search/search.py index 4e0a3f8d..bf948209 100644 --- a/src/biscuit/views/search/search.py +++ b/src/biscuit/views/search/search.py @@ -20,7 +20,7 @@ class Search(NavigationDrawerView): """ def __init__(self, master, *args, **kwargs) -> None: - self.__buttons__ = (("refresh",), ("clear-all",), ("collapse-all",)) + self.__actions__ = (("refresh",), ("clear-all",), ("collapse-all",)) super().__init__(master, *args, **kwargs) self.__icon__ = "search" self.name = "Search" diff --git a/src/biscuit/views/sourcecontrol/changes.py b/src/biscuit/views/sourcecontrol/changes.py index 3507dc93..da981b6a 100644 --- a/src/biscuit/views/sourcecontrol/changes.py +++ b/src/biscuit/views/sourcecontrol/changes.py @@ -13,7 +13,7 @@ class Changes(NavigationDrawerViewItem): """ def __init__(self, master, *args, **kwargs) -> None: - self.__buttons__ = ( + self.__actions__ = ( ("discard", self.git_discard_all), ("add", self.git_add_all), ) diff --git a/src/biscuit/views/sourcecontrol/stagedchanges.py b/src/biscuit/views/sourcecontrol/stagedchanges.py index 0ad744d9..52642a0b 100644 --- a/src/biscuit/views/sourcecontrol/stagedchanges.py +++ b/src/biscuit/views/sourcecontrol/stagedchanges.py @@ -12,7 +12,7 @@ class StagedChanges(NavigationDrawerViewItem): """ def __init__(self, master, *args, **kwargs) -> None: - self.__buttons__ = (("remove", self.git_remove_all),) + self.__actions__ = (("remove", self.git_remove_all),) self.title = "Staged Changes" super().__init__(master, *args, **kwargs) self.config(**self.base.theme.views.sidebar.item) diff --git a/src/biscuit/views/terminal/ai.py b/src/biscuit/views/terminal/ai.py new file mode 100644 index 00000000..d592d8ce --- /dev/null +++ b/src/biscuit/views/terminal/ai.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +import typing + +import google.generativeai as ai +from google.generativeai.types import HarmBlockThreshold, HarmCategory + +if typing.TYPE_CHECKING: + from .terminalbase import TerminalBase + + +class AI: + def __init__(self, terminal: TerminalBase) -> None: + self.terminal = terminal + self.base = terminal.base + + self.prompt = f"""You are an expert at terminal commands. you know all the commands + and their arguments. You can generate correct commands with arguments from the command + that was not recognized by the terminal. You can recognize and fix commands from any shell + in any operating system. Only return the fixed command, and dont explain it. + + The user may talk in english as well, its when he is not sure how to write the command. + You can also generate commands for the user if he is not sure what to do! + + Shell executable used: {self.terminal.shell} + Shell name: {self.terminal.name} + System details: {str(self.base.system)} + Input from user: """ + + self.model = ai.GenerativeModel( + "gemini-1.5-flash", + safety_settings={ + HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_NONE, + HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_NONE, + HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_NONE, + HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_NONE, + }, + ) + self.chat = self.model.start_chat() + + def get_gemini_response(self, err_command: str) -> str: + try: + response = self.chat.send_message([self.prompt, err_command]) + if response and response.text: + self.base.palette.show("runc:", response.text.strip()) + except Exception as e: + self.base.logger.error(e) + self.base.drawer.show_ai().add_placeholder() diff --git a/src/biscuit/views/terminal/terminal.py b/src/biscuit/views/terminal/terminal.py index b57557cf..dc31970b 100644 --- a/src/biscuit/views/terminal/terminal.py +++ b/src/biscuit/views/terminal/terminal.py @@ -3,6 +3,8 @@ import subprocess import tkinter as tk +from src.biscuit.common.actionset import ActionSet + from ..panelview import PanelView from .menu import TerminalMenu from .shells import SHELLS, Default @@ -38,7 +40,7 @@ def __init__(self, master, *args, **kwargs) -> None: self.menu = TerminalMenu(self, "terminal") self.menu.add_command("Clear Terminal", self.clear_terminal) - self.__buttons__ = [ + self.__actions__ = [ ("add", self.addmenu.show), ("trash", self.delete_active_terminal), ("ellipsis", self.menu.show), @@ -49,6 +51,13 @@ def __init__(self, master, *args, **kwargs) -> None: self.active_terminals = [] + self.run_actionset = ActionSet( + "Run Command in Terminal", + "runc:", + pinned=[["Run command? {}", self.run_command]], + ) + self.base.palette.register_actionset(lambda: self.run_actionset) + def add_default_terminal(self) -> Default: default_terminal = Default( self, cwd=self.base.active_directory or get_home_directory() diff --git a/src/biscuit/views/terminal/terminalbase.py b/src/biscuit/views/terminal/terminalbase.py index a15e7809..20a896b5 100644 --- a/src/biscuit/views/terminal/terminalbase.py +++ b/src/biscuit/views/terminal/terminalbase.py @@ -10,6 +10,7 @@ from src.biscuit.common.ui import Scrollbar from ..panelview import PanelView +from .ai import AI from .ansi import replace_newline, strip_ansi_escape_sequences from .text import TerminalText @@ -50,13 +51,16 @@ def __init__(self, master, cwd=".", *args, **kwargs) -> None: cwd: The current working directory""" super().__init__(master, *args, **kwargs) - self.__buttons__ = (("add",), ("trash", self.destroy)) + self.__actions__ = (("add",), ("trash", self.destroy)) self.grid_columnconfigure(0, weight=1) self.grid_rowconfigure(0, weight=1) self.alive = False self.cwd = cwd + self.last_command = "" + self.last_command_index = "" + # self.prediction = "" self.text = TerminalText( self, relief=tk.FLAT, padx=10, pady=10, font=("Consolas", 11) @@ -72,8 +76,12 @@ def __init__(self, master, cwd=".", *args, **kwargs) -> None: self.text.tag_config("prompt", foreground=self.base.theme.biscuit_dark) self.text.tag_config("command", foreground=self.base.theme.biscuit) + self.text.tag_config("ghost", foreground=self.base.theme.border) + + self.ai = AI(self) self.bind("", self.destroy) + # self.bind("", self.tab_key) def start_service(self, *_) -> None: self.alive = True @@ -90,11 +98,14 @@ def run_command(self, command: str) -> None: self.enter() def enter(self, *_) -> None: + self.last_command_index = self.text.index("input") + command = self.text.get("input", "end") self.last_command = command self.text.register_history(command) if command.strip(): self.text.delete("input", "end") + self.text.tag_add("prompt", "input linestart", "input") self.p.write(command + "\r\n") return "break" @@ -110,7 +121,17 @@ def write_loop(self) -> None: strip_ansi_escape_sequences(i) for i in replace_newline(buf).splitlines() ] - self.insert("\n".join(buf)) + buf = "\n".join(buf) + + self.insert(buf) + if "is not recognized as an internal or external command" in buf: + self.error() + + def error(self): + if not self.base.ai.api_key: + self.base.drawer.show_ai().add_placeholder() + + self.ai.get_gemini_response(self.last_command) def insert(self, output: str, tag="") -> None: self.text.insert(tk.END, output, tag) @@ -124,6 +145,19 @@ def newline(self): def clear(self) -> None: self.text.clear() + # def ghost_insert(self, output: str) -> None: + # self.text.insert(tk.END, output, "ghost") + # self.text.see(tk.END) + # self.prediction = output + + # def tab_key(self, *_) -> None: + # if t := self.text.get("input", "insert"): + # if t.strip() in self.prediction.strip(): + # self.text.delete("input", "end") + # self.text.insert("end", self.prediction) + # else: + # self.text.insert("end", " ") + def ctrl_key(self, key: str) -> None: if key == "c": self.run_command("\x03") diff --git a/src/biscuit/views/toolbar.py b/src/biscuit/views/toolbar.py index 7642c550..6a9d39f9 100644 --- a/src/biscuit/views/toolbar.py +++ b/src/biscuit/views/toolbar.py @@ -32,15 +32,15 @@ def __init__(self, master, title: str, buttons=(), *args, **kwargs) -> None: self.actions = Frame(self, **self.base.theme.views.sidebar.itembar) self.actions.grid(row=0, column=2, sticky=tk.E) - self.add_buttons(buttons) + self.add_actions(buttons) - def add_button(self, *args) -> None: + def add_action(self, *args) -> None: IconButton(self.actions, *args).grid(row=0, column=self.buttoncolumn) self.buttoncolumn += 1 - def add_buttons(self, buttons) -> None: + def add_actions(self, buttons) -> None: for btn in buttons: - self.add_button(*btn) + self.add_action(*btn) def set_title(self, title: str) -> None: self.title.set(title.upper())