├── .gitignore ├── LICENSE ├── README.md ├── arguments.md ├── commands.md ├── img ├── close.png ├── console.jpg ├── console_2.jpg ├── find.png ├── palette.png ├── readme_1.png ├── readme_2.png ├── readme_3.png ├── snippets.png ├── spanish.png ├── system.png ├── theme_contrast.png ├── theme_dark.png ├── theme_light.png └── upload.jpg ├── keyboard.md ├── llama_reqs.txt ├── meltdown ├── __init__.py ├── app.py ├── argparser.py ├── args.py ├── argspec.py ├── autocomplete.py ├── autoscroll.py ├── book.py ├── bottom.py ├── buttonbox.py ├── changes.py ├── close.py ├── command_spec.py ├── commands.py ├── config.py ├── console.py ├── contrast_theme.py ├── dark_theme.py ├── delete.py ├── details.py ├── dialogs.py ├── display.py ├── entrybox.py ├── filecontrol.py ├── files.py ├── find.py ├── findmanager.py ├── formats.py ├── framedata.py ├── gestures.py ├── icon.png ├── image.jpg ├── inputcontrol.py ├── itemops.py ├── keyboard.py ├── light_theme.py ├── listener.py ├── logs.py ├── main.py ├── manifest.json ├── markdown.py ├── memory.py ├── menumanager.py ├── menus.py ├── model.py ├── modelcontrol.py ├── nouns.txt ├── output.py ├── paths.py ├── portrait.jpg ├── rentry.py ├── ruff.toml ├── scrollers.py ├── separatorbox.py ├── session.py ├── signals.py ├── snippet.py ├── summarize.py ├── system.py ├── system_prompt.py ├── tasks.py ├── tests.py ├── textbox.py ├── theme.py ├── tips.py ├── tooltips.py ├── upload.py ├── utils.py ├── variables.py ├── widgets.py └── widgetutils.py ├── requirements.txt ├── run.sh ├── scripts ├── add_llama.sh ├── add_llama_amd.sh ├── check.sh ├── count.sh ├── makedocs.sh ├── search.sh ├── tag.py └── venv.sh └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | venv/* 2 | output/* 3 | *.pyc 4 | __pycache__/ 5 | .mypy_cache/ 6 | Meltdown.egg-info/ 7 | build/ 8 | dist/ 9 | main.spec 10 | run.spec 11 | decomp/ 12 | *.tar.gz 13 | *.zip 14 | llama.log 15 | *.xml 16 | *.iml 17 | *.idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Merkoba - All Rights Reserved -------------------------------------------------------------------------------- /img/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Merkoba/Meltdown/4265a033c2a5a5845ee03810871cf712e7909a1f/img/close.png -------------------------------------------------------------------------------- /img/console.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Merkoba/Meltdown/4265a033c2a5a5845ee03810871cf712e7909a1f/img/console.jpg -------------------------------------------------------------------------------- /img/console_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Merkoba/Meltdown/4265a033c2a5a5845ee03810871cf712e7909a1f/img/console_2.jpg -------------------------------------------------------------------------------- /img/find.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Merkoba/Meltdown/4265a033c2a5a5845ee03810871cf712e7909a1f/img/find.png -------------------------------------------------------------------------------- /img/palette.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Merkoba/Meltdown/4265a033c2a5a5845ee03810871cf712e7909a1f/img/palette.png -------------------------------------------------------------------------------- /img/readme_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Merkoba/Meltdown/4265a033c2a5a5845ee03810871cf712e7909a1f/img/readme_1.png -------------------------------------------------------------------------------- /img/readme_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Merkoba/Meltdown/4265a033c2a5a5845ee03810871cf712e7909a1f/img/readme_2.png -------------------------------------------------------------------------------- /img/readme_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Merkoba/Meltdown/4265a033c2a5a5845ee03810871cf712e7909a1f/img/readme_3.png -------------------------------------------------------------------------------- /img/snippets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Merkoba/Meltdown/4265a033c2a5a5845ee03810871cf712e7909a1f/img/snippets.png -------------------------------------------------------------------------------- /img/spanish.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Merkoba/Meltdown/4265a033c2a5a5845ee03810871cf712e7909a1f/img/spanish.png -------------------------------------------------------------------------------- /img/system.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Merkoba/Meltdown/4265a033c2a5a5845ee03810871cf712e7909a1f/img/system.png -------------------------------------------------------------------------------- /img/theme_contrast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Merkoba/Meltdown/4265a033c2a5a5845ee03810871cf712e7909a1f/img/theme_contrast.png -------------------------------------------------------------------------------- /img/theme_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Merkoba/Meltdown/4265a033c2a5a5845ee03810871cf712e7909a1f/img/theme_dark.png -------------------------------------------------------------------------------- /img/theme_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Merkoba/Meltdown/4265a033c2a5a5845ee03810871cf712e7909a1f/img/theme_light.png -------------------------------------------------------------------------------- /img/upload.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Merkoba/Meltdown/4265a033c2a5a5845ee03810871cf712e7909a1f/img/upload.jpg -------------------------------------------------------------------------------- /keyboard.md: -------------------------------------------------------------------------------- 1 | # Keyboard Shortcuts 2 | 3 | --- 4 | 5 | < TAB > 6 | 7 | Autocomplete commands 8 | 9 | --- 10 | 11 | < RETURN > 12 | 13 | Submit prompt 14 | 15 | Ctrl: Submit without using history 16 | 17 | Shift: Show the Write textbox and add a line 18 | 19 | --- 20 | 21 | < ESCAPE > 22 | 23 | Clear input, select active, stop model stream, go to bottom 24 | 25 | Ctrl: Unload model 26 | 27 | Shift: Open task manager 28 | 29 | --- 30 | 31 | < PRIOR > 32 | 33 | Scroll up 34 | 35 | Ctrl: Scroll up more 36 | 37 | Shift: Scroll up more 38 | 39 | --- 40 | 41 | < NEXT > 42 | 43 | Scroll down 44 | 45 | Ctrl: Scroll down more 46 | 47 | Shift: Scroll down more 48 | 49 | --- 50 | 51 | < UP > 52 | 53 | History up 54 | 55 | Ctrl: Scroll to top 56 | 57 | Shift: Show context 58 | 59 | --- 60 | 61 | < DOWN > 62 | 63 | History down 64 | 65 | Ctrl: Scroll to bottom 66 | 67 | --- 68 | 69 | , 70 | 71 | Ctrl: Go to the next tab (left) 72 | 73 | --- 74 | 75 | . 76 | 77 | Ctrl: Go to the next tab (right) 78 | 79 | --- 80 | 81 | < LESS > 82 | 83 | Ctrl+Shift: Move tab to the left 84 | 85 | --- 86 | 87 | < GREATER > 88 | 89 | Ctrl+Shift: Move tab to the right 90 | 91 | --- 92 | 93 | < SPACE > 94 | 95 | Ctrl: Show the Write textbox 96 | 97 | --- 98 | 99 | F 100 | 101 | Ctrl: Find text 102 | 103 | --- 104 | 105 | T 106 | 107 | Ctrl: Make tab 108 | 109 | --- 110 | 111 | N 112 | 113 | Ctrl: Make tab 114 | 115 | --- 116 | 117 | W 118 | 119 | Ctrl: Close tab 120 | 121 | --- 122 | 123 | S 124 | 125 | Ctrl: Save session 126 | 127 | --- 128 | 129 | O 130 | 131 | Ctrl: Load session 132 | 133 | --- 134 | 135 | Y 136 | 137 | Ctrl+Shift: Copy conversation 138 | 139 | --- 140 | 141 | P 142 | 143 | Ctrl: Go to the previous tab 144 | 145 | --- 146 | 147 | R 148 | 149 | Ctrl: Resize window 150 | 151 | --- 152 | 153 | M 154 | 155 | Ctrl: Show the main menu 156 | 157 | --- 158 | 159 | L 160 | 161 | Ctrl: Show the log menu 162 | 163 | Ctrl+Shift: Open the last log 164 | 165 | --- 166 | 167 | < KP_ADD > 168 | 169 | Ctrl: Increase the font size 170 | 171 | Ctrl+Shift: Reset the font size 172 | 173 | --- 174 | 175 | < KP_SUBTRACT > 176 | 177 | Ctrl: Decrease the font size 178 | 179 | Ctrl+Shift: Reset the font size 180 | 181 | --- 182 | 183 | < EQUAL > 184 | 185 | Ctrl: Increase the font size 186 | 187 | --- 188 | 189 | < MINUS > 190 | 191 | Ctrl: Decrease the font size 192 | 193 | --- 194 | 195 | 0 196 | 197 | Ctrl: Reset the font size 198 | 199 | --- 200 | 201 | 1 to 9 to jump to tabs 202 | 203 | --- 204 | 205 | F1 to F12 to run commands (configurable through arguments) 206 | 207 | F1 = /help 208 | 209 | F2 = /findprev 210 | 211 | F3 = /findnext 212 | 213 | F4 = /close 214 | 215 | F5 = /reset 216 | 217 | F6 = /delete 218 | 219 | F7 = /clear 220 | 221 | F8 = /compact 222 | 223 | F9 = /autoscroll 224 | 225 | F10 = /logtext 226 | 227 | F11 = /fullscreen 228 | 229 | F12 = /list 230 | -------------------------------------------------------------------------------- /llama_reqs.txt: -------------------------------------------------------------------------------- 1 | llama_cpp_python == 0.3.8 -------------------------------------------------------------------------------- /meltdown/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Merkoba/Meltdown/4265a033c2a5a5845ee03810871cf712e7909a1f/meltdown/__init__.py -------------------------------------------------------------------------------- /meltdown/argparser.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | # Standard 4 | import argparse 5 | from typing import Any 6 | 7 | 8 | class ArgParser: 9 | def __init__(self, title: str, argdefs: dict[str, Any], obj: Any): 10 | parser = argparse.ArgumentParser(description=title) 11 | argdefs["string_arg"] = {"nargs": "*"} 12 | 13 | for key in argdefs: 14 | item = argdefs[key] 15 | 16 | if key == "string_arg": 17 | name = key 18 | else: 19 | name = self.under_to_dash(key) 20 | name = f"--{name}" 21 | 22 | tail = {key: value for key, value in item.items() if value is not None} 23 | parser.add_argument(name, **tail) 24 | 25 | self.parser = parser 26 | self.args = parser.parse_args() 27 | self.obj = obj 28 | 29 | def string_arg(self) -> str: 30 | return " ".join(self.args.string_arg) 31 | 32 | def get_value( 33 | self, attr: str, key: str | None = None, no_strip: bool = False 34 | ) -> None: 35 | value = getattr(self.args, attr) 36 | 37 | if value is not None: 38 | if not no_strip: 39 | if isinstance(value, str): 40 | value = value.strip() 41 | 42 | obj = key if key else attr 43 | self.set(obj, value) 44 | 45 | def set(self, attr: str, value: Any) -> None: 46 | setattr(self.obj, attr, value) 47 | 48 | def under_to_dash(self, s: str) -> str: 49 | return s.replace("_", "-") 50 | -------------------------------------------------------------------------------- /meltdown/autocomplete.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | # Standard 4 | import tkinter as tk 5 | 6 | # Modules 7 | from .inputcontrol import inputcontrol 8 | from .commands import commands 9 | from .args import args 10 | from .utils import utils 11 | from .entrybox import EntryBox 12 | from .textbox import TextBox 13 | from .variables import variables 14 | 15 | 16 | InputWidget = EntryBox | TextBox | None 17 | 18 | 19 | class AutoComplete: 20 | def __init__(self) -> None: 21 | self.index = 0 22 | self.matches: list[str] = [] 23 | self.match = "" 24 | self.word = "" 25 | self.pos = 0 26 | self.missing = "" 27 | self.widget: InputWidget 28 | 29 | def check(self, widget: InputWidget = None) -> None: 30 | try: 31 | self.do_check(widget=widget) 32 | except BaseException as e: 33 | utils.error(e) 34 | 35 | def do_check(self, widget: InputWidget = None) -> None: 36 | if not widget: 37 | self.widget = inputcontrol.input 38 | else: 39 | self.widget = widget 40 | 41 | if not self.widget: 42 | return 43 | 44 | text = self.widget.get_text() 45 | 46 | if "\n" in text: 47 | text = text.split("\n")[-1] 48 | 49 | if not self.matches: 50 | self.get_matches(text) 51 | 52 | def action() -> None: 53 | if not self.widget: 54 | return 55 | 56 | if not isinstance(self.widget, EntryBox): 57 | return 58 | 59 | if self.index >= len(self.matches): 60 | self.index = 0 61 | 62 | match = self.matches[self.index] 63 | input_text = text[1:] 64 | 65 | if match == input_text: 66 | if len(self.matches) == 1: 67 | return 68 | 69 | self.index += 1 70 | action() 71 | return 72 | 73 | missing = match[len(self.clean(self.word)) :] 74 | 75 | if self.match: 76 | self.widget.delete_text(self.pos, len(self.missing)) 77 | 78 | self.widget.insert_text(missing, index=self.pos, add_space=True) 79 | 80 | self.index += 1 81 | self.match = match 82 | self.missing = missing 83 | 84 | if self.matches: 85 | action() 86 | 87 | def reset(self) -> None: 88 | self.matches = [] 89 | self.match = "" 90 | self.missing = "" 91 | self.index = 0 92 | self.pos = 0 93 | 94 | def get_matches(self, text: str) -> None: 95 | from .inputcontrol import inputcontrol 96 | 97 | if not text: 98 | return 99 | 100 | if not self.widget: 101 | return 102 | 103 | self.reset() 104 | s_caret_pos = str(self.widget.index(tk.INSERT)) 105 | 106 | if "." in s_caret_pos: 107 | s_caret_pos = s_caret_pos.split(".")[1] 108 | 109 | self.pos = int(s_caret_pos) 110 | text_to_caret = text[: self.pos] 111 | last_space_pos = text_to_caret.rfind(" ") 112 | word = text_to_caret[last_space_pos + 1 : self.pos] 113 | 114 | if not word: 115 | return 116 | 117 | self.word = word 118 | 119 | if commands.is_command(word): 120 | word = self.clean(word) 121 | 122 | for key in commands.cmdkeys: 123 | if key.startswith(word): 124 | self.matches.append(key) 125 | elif variables.is_variable(word): 126 | word = self.clean(word) 127 | 128 | for key in variables.variables: 129 | if key.startswith(word): 130 | self.matches.append(key) 131 | else: 132 | for w in inputcontrol.autocomplete: 133 | if w.startswith(word): 134 | self.matches.append(w) 135 | 136 | def clean(self, text: str) -> str: 137 | if text.startswith(args.command_prefix): 138 | return text[1:] 139 | 140 | if text.startswith(args.variable_prefix): 141 | return text[1:] 142 | 143 | return text 144 | 145 | 146 | autocomplete = AutoComplete() 147 | -------------------------------------------------------------------------------- /meltdown/autoscroll.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | # Modules 4 | from .app import app 5 | from .args import args 6 | from .utils import utils 7 | 8 | 9 | class AutoScroll: 10 | def __init__(self) -> None: 11 | self.enabled = False 12 | self.direction = "down" 13 | self.delay_diff = 100 14 | self.delay = 1000 15 | self.min_delay = 100 16 | self.max_delay = 2000 17 | 18 | def setup(self) -> None: 19 | self.delay = utils.clamp(args.autoscroll_delay, self.min_delay, self.max_delay) 20 | 21 | def start(self, direction: str | None = None) -> None: 22 | from .display import display 23 | 24 | if self.enabled: 25 | return 26 | 27 | tab = display.get_current_tab() 28 | 29 | if not tab: 30 | return 31 | 32 | if not direction: 33 | direction = "down" 34 | 35 | if direction == "up": 36 | if tab.get_output().yview()[0] <= 0.0001: 37 | return 38 | elif direction == "down": 39 | if tab.get_output().yview()[1] >= 0.9999: 40 | return 41 | else: 42 | return 43 | 44 | self.direction = direction 45 | self.enabled = True 46 | tab.get_bottom().on_autoscroll_enabled() 47 | self.schedule_autoscroll() 48 | 49 | def stop(self, check: bool = False) -> None: 50 | from .display import display 51 | 52 | if not self.enabled: 53 | return 54 | 55 | if check: 56 | if not args.autoscroll_interrupt: 57 | return 58 | 59 | self.enabled = False 60 | 61 | tab = display.get_current_tab() 62 | 63 | if not tab: 64 | return 65 | 66 | tab.get_bottom().on_autoscroll_disabled() 67 | 68 | def toggle(self, direction: str | None = None) -> None: 69 | if not direction: 70 | direction = "down" 71 | 72 | if self.enabled: 73 | if direction != self.direction: 74 | self.direction = direction 75 | return 76 | 77 | self.stop() 78 | else: 79 | self.start(direction=direction) 80 | 81 | def check(self) -> None: 82 | from .display import display 83 | 84 | if self.delay < 100: 85 | return 86 | 87 | if not self.enabled: 88 | return 89 | 90 | if self.direction == "up": 91 | display.scroll_up() 92 | else: 93 | display.scroll_down() 94 | 95 | self.schedule_autoscroll() 96 | 97 | def schedule_autoscroll(self) -> None: 98 | app.root.after(self.delay, lambda: self.check()) 99 | 100 | def faster(self) -> None: 101 | delay = max(self.delay - self.delay_diff, self.min_delay) 102 | self.update_delay(delay) 103 | 104 | def slower(self) -> None: 105 | delay = min(self.delay + self.delay_diff, self.max_delay) 106 | self.update_delay(delay) 107 | 108 | def slowest(self) -> None: 109 | self.update_delay(self.max_delay) 110 | 111 | def fastest(self) -> None: 112 | self.update_delay(self.min_delay) 113 | 114 | def update_delay(self, delay: int) -> None: 115 | self.delay = delay 116 | self.update_text() 117 | 118 | def update_text(self) -> None: 119 | from .display import display 120 | 121 | tab = display.get_current_tab() 122 | 123 | if not tab: 124 | return 125 | 126 | tab.get_bottom().autoscroll_button.set_text(self.get_text()) 127 | 128 | def get_text(self) -> str: 129 | a = self.delay - self.min_delay 130 | b = self.max_delay - self.min_delay 131 | perc = 100 - int((a / b) * 100) 132 | return f"Autoscroll ({perc})" 133 | 134 | def reset(self) -> None: 135 | self.setup() 136 | self.update_text() 137 | 138 | 139 | autoscroll = AutoScroll() 140 | -------------------------------------------------------------------------------- /meltdown/bottom.py: -------------------------------------------------------------------------------- 1 | # Standard 2 | import tkinter as tk 3 | from typing import Any 4 | 5 | # Modules 6 | from .app import app 7 | from .args import args 8 | 9 | 10 | # Modules 11 | from .buttonbox import ButtonBox 12 | from .tooltips import ToolTip 13 | from .tips import tips 14 | from .autoscroll import autoscroll 15 | 16 | 17 | class Bottom(tk.Frame): 18 | def __init__(self, parent: tk.Frame, tab_id: str) -> None: 19 | super().__init__(parent) 20 | self.bottom_text = "Go To Bottom" 21 | 22 | self.bottom_button = ButtonBox( 23 | self, text=self.bottom_text, command=self.to_bottom 24 | ) 25 | 26 | ToolTip(self.bottom_button, tips["bottom_button"]) 27 | self.bottom_button.grid(row=0, column=0, sticky="nsew") 28 | self.bottom_button.set_bind("", lambda e: self.scroll_up()) 29 | self.bottom_button.set_bind("", lambda e: self.scroll_down()) 30 | self.bottom_button.set_bind("", lambda e: self.autoscroll_down()) 31 | self.bottom_button.set_bind("", lambda e: self.to_top()) 32 | 33 | self.autoscroll_slower_button = ButtonBox( 34 | self, text="-", command=autoscroll.slower, style="alt" 35 | ) 36 | 37 | self.autoscroll_button = ButtonBox( 38 | self, text=autoscroll.get_text(), command=self.autoscroll_down, style="alt" 39 | ) 40 | 41 | self.autoscroll_faster_button = ButtonBox( 42 | self, text="+", command=autoscroll.faster, style="alt" 43 | ) 44 | 45 | self.autoscroll_slower_button.set_bind( 46 | "", lambda e: autoscroll.slowest() 47 | ) 48 | 49 | self.autoscroll_faster_button.set_bind( 50 | "", lambda e: autoscroll.fastest() 51 | ) 52 | 53 | ToolTip(self.autoscroll_slower_button, tips["autoscroll_slower"]) 54 | ToolTip(self.autoscroll_button, tips["autoscroll"]) 55 | ToolTip(self.autoscroll_faster_button, tips["autoscroll_faster"]) 56 | 57 | if args.show_autoscroll: 58 | self.autoscroll_slower_button.grid(row=0, column=1, sticky="nsew") 59 | self.autoscroll_button.grid(row=0, column=2, sticky="nsew") 60 | self.autoscroll_faster_button.grid(row=0, column=3, sticky="nsew") 61 | 62 | self.autoscroll_button.set_bind("", lambda e: autoscroll.reset()) 63 | self.autoscroll_button.set_bind("", lambda e: self.autoscroll_up()) 64 | 65 | def autoscroll_wheel(button: Any) -> None: 66 | button.set_bind("", lambda e: autoscroll.faster()) 67 | button.set_bind("", lambda e: autoscroll.slower()) 68 | 69 | autoscroll_wheel(self.autoscroll_slower_button) 70 | autoscroll_wheel(self.autoscroll_button) 71 | autoscroll_wheel(self.autoscroll_faster_button) 72 | 73 | self.grid_rowconfigure(0, weight=1) 74 | self.grid_columnconfigure(0, weight=1) 75 | 76 | self.parent = parent 77 | self.tab_id = tab_id 78 | self.visible = True 79 | self.delay = 500 80 | self.show_debouncer = "" 81 | self.buttons_enabled = True 82 | self.grid(row=2, column=0, sticky="nsew") 83 | 84 | if args.bottom_autohide: 85 | self.grid_remove() 86 | 87 | def do_show(self) -> None: 88 | if (not args.bottom) or (not self.visible): 89 | return 90 | 91 | self.grid() 92 | 93 | def cancel_show(self) -> None: 94 | if not self.show_debouncer: 95 | return 96 | 97 | app.root.after_cancel(self.show_debouncer) 98 | self.show_debouncer = "" 99 | 100 | def show(self) -> None: 101 | if not args.bottom_autohide: 102 | self.bottom_button.set_style("normal") 103 | self.bottom_button.set_text(self.bottom_text) 104 | self.autoscroll_button.set_text(autoscroll.get_text()) 105 | self.check_autoscroll() 106 | self.buttons_enabled = True 107 | return 108 | 109 | if (not args.bottom) or self.visible or (not app.exists()): 110 | return 111 | 112 | self.check_autoscroll() 113 | self.visible = True 114 | self.show_debouncer = app.root.after(self.delay, self.do_show) 115 | 116 | def hide(self) -> None: 117 | if not args.bottom_autohide: 118 | self.bottom_button.set_style("disabled") 119 | self.bottom_button.set_text("") 120 | self.autoscroll_button.set_style("disabled") 121 | self.autoscroll_button.set_text("") 122 | self.buttons_enabled = False 123 | return 124 | 125 | if (not self.visible) or (not app.exists()): 126 | return 127 | 128 | self.cancel_show() 129 | self.visible = False 130 | self.grid_remove() 131 | 132 | def to_top(self) -> None: 133 | from .display import display 134 | 135 | display.to_top(self.tab_id) 136 | 137 | def to_bottom(self) -> None: 138 | from .display import display 139 | 140 | display.to_bottom(self.tab_id) 141 | 142 | def scroll_up(self) -> None: 143 | from .display import display 144 | 145 | ToolTip.hide_all() 146 | display.scroll_up(self.tab_id, disable_autoscroll=True) 147 | 148 | def scroll_down(self) -> None: 149 | from .display import display 150 | 151 | ToolTip.hide_all() 152 | display.scroll_down(self.tab_id, disable_autoscroll=True) 153 | 154 | def set_text(self, text: str) -> None: 155 | self.bottom_button.set_text(text) 156 | 157 | def autoscroll_up(self) -> None: 158 | autoscroll.toggle("up") 159 | 160 | def autoscroll_down(self) -> None: 161 | autoscroll.toggle("down") 162 | 163 | def check_enabled(self) -> bool: 164 | return self.visible and self.buttons_enabled and app.exists() 165 | 166 | def on_autoscroll_enabled(self) -> None: 167 | if not self.check_enabled(): 168 | return 169 | 170 | self.autoscroll_button.set_style("active") 171 | 172 | def on_autoscroll_disabled(self) -> None: 173 | if not self.check_enabled(): 174 | return 175 | 176 | self.autoscroll_button.set_style("alt") 177 | 178 | def check_autoscroll(self) -> None: 179 | if autoscroll.enabled: 180 | self.autoscroll_button.set_style("active") 181 | else: 182 | self.autoscroll_button.set_style("alt") 183 | -------------------------------------------------------------------------------- /meltdown/buttonbox.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | # Standard 4 | import inspect 5 | import tkinter as tk 6 | from typing import Any 7 | from collections.abc import Callable 8 | 9 | # Modules 10 | from .app import app 11 | from .menus import Menu 12 | 13 | 14 | class ButtonBox(tk.Frame): 15 | def __init__( 16 | self, 17 | parent: tk.Frame, 18 | text: str, 19 | command: Callable[..., Any] | None = None, 20 | when: str | None = None, 21 | style: str | None = None, 22 | width: int | None = None, 23 | ) -> None: 24 | super().__init__(parent) 25 | self.text = text 26 | style = style if style else "normal" 27 | when = when if when else "" 28 | self.custom_width = width is not None 29 | self.width = width if width else app.theme.button_width 30 | self.command = command 31 | self.make() 32 | self.set_style(style) 33 | 34 | if command: 35 | self.set_bind(when, command) 36 | 37 | def prepare_text(self, text: str) -> str: 38 | if (not self.custom_width) and (len(text) < 4): 39 | space = " " 40 | text = f"{space}{text}{space}" 41 | 42 | return text 43 | 44 | def make(self) -> None: 45 | pady = 0 46 | padx = app.theme.button_padx 47 | text = self.prepare_text(self.text) 48 | 49 | self.label = tk.Label( 50 | self, text=text, font=app.theme.font("button"), padx=padx, pady=pady 51 | ) 52 | 53 | self.label.grid(sticky="nsew") 54 | self.grid_columnconfigure(0, weight=1) 55 | self.grid_rowconfigure(0, weight=1) 56 | self.label.bind("", lambda e: self.on_enter()) 57 | self.label.bind("", lambda e: self.on_leave()) 58 | 59 | def on_enter(self) -> None: 60 | if self.style == "normal": 61 | self.set_background(app.theme.button_hover_background) 62 | elif self.style == "highlight": 63 | self.set_background(app.theme.button_highlight_hover_background) 64 | elif self.style == "active": 65 | self.set_background(app.theme.button_active_hover_background) 66 | elif self.style == "alt": 67 | self.set_background(app.theme.button_alt_hover_background) 68 | 69 | def on_leave(self) -> None: 70 | if self.style == "normal": 71 | self.set_background(app.theme.button_background) 72 | elif self.style == "highlight": 73 | self.set_background(app.theme.button_highlight_background) 74 | elif self.style == "active": 75 | self.set_background(app.theme.button_active_background) 76 | elif self.style == "disabled": 77 | self.set_background(app.theme.button_disabled_background) 78 | elif self.style == "alt": 79 | self.set_background(app.theme.button_alt_background) 80 | 81 | def set_background(self, color: str) -> None: 82 | self.configure(background=color) 83 | self.label.configure(background=color) 84 | 85 | def set_bind(self, when: str, command: Callable[..., Any]) -> None: 86 | # Check if press/release happens on top of the button 87 | def on_top(event: Any) -> bool: 88 | widget = event.widget 89 | x = widget.winfo_x() 90 | y = widget.winfo_y() 91 | width = widget.winfo_width() 92 | height = widget.winfo_height() 93 | return bool((x <= event.x <= x + width) and (y <= event.y <= y + height)) 94 | 95 | def cmd(event: Any) -> None: 96 | if self.style == "disabled": 97 | if (when == "") or (when == ""): 98 | Menu.hide_all() 99 | return 100 | 101 | if not on_top(event): 102 | return 103 | 104 | event.widget.focus_set() 105 | 106 | if inspect.signature(command).parameters: 107 | command(event) 108 | else: 109 | command() 110 | 111 | self.bind(when, lambda e: cmd(e)) 112 | self.label.bind(when, lambda e: cmd(e)) 113 | 114 | def set_style(self, style: str) -> None: 115 | self.label.configure(foreground=app.theme.button_foreground) 116 | 117 | if style == "normal": 118 | self.set_background(app.theme.button_background) 119 | self.configure(cursor="hand2") 120 | elif style == "highlight": 121 | self.set_background(app.theme.button_highlight_background) 122 | self.configure(cursor="hand2") 123 | elif style == "active": 124 | self.set_background(app.theme.button_active_background) 125 | self.configure(cursor="hand2") 126 | elif style == "disabled": 127 | self.set_background(app.theme.button_disabled_background) 128 | self.configure(cursor="arrow") 129 | elif style == "alt": 130 | self.set_background(app.theme.button_alt_background) 131 | self.configure(cursor="hand2") 132 | 133 | self.style = style 134 | 135 | def set_text(self, text: str) -> None: 136 | self.text = text.strip() 137 | text = self.prepare_text(self.text) 138 | self.label.configure(text=text) 139 | 140 | def get_text(self) -> str: 141 | return self.text 142 | 143 | def set_font(self, font: tuple[str, int, str]) -> None: 144 | self.label.configure(font=font) 145 | -------------------------------------------------------------------------------- /meltdown/changes.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | # Modules 4 | from .app import app 5 | from .config import config 6 | from .entrybox import EntryBox 7 | from .textbox import TextBox 8 | 9 | 10 | class Changes: 11 | def __init__(self, widget: EntryBox | TextBox) -> None: 12 | self.widget = widget 13 | self.changes_delay = config.changes_delay 14 | self.changes_after = "" 15 | self.changes: list[str] = [""] 16 | self.changes_index = 0 17 | 18 | widget.bind("", lambda e: self.undo()) 19 | widget.bind("", lambda e: self.undo()) 20 | widget.bind("", lambda e: self.redo()) 21 | widget.bind("", lambda e: self.redo()) 22 | 23 | def undo(self) -> None: 24 | self.changes_index -= 1 25 | 26 | if self.changes_index < 0: 27 | self.changes_index = 0 28 | return 29 | 30 | text = self.changes[self.changes_index] 31 | self.widget.set_text(text, on_change=False) 32 | 33 | def redo(self) -> None: 34 | self.changes_index += 1 35 | 36 | if self.changes_index >= len(self.changes): 37 | self.changes_index = len(self.changes) - 1 38 | return 39 | 40 | text = self.changes[self.changes_index] 41 | self.widget.set_text(text, on_change=False) 42 | 43 | def on_change(self) -> None: 44 | if self.changes_after: 45 | app.root.after_cancel(self.changes_after) 46 | 47 | self.changes_after = app.root.after( 48 | self.changes_delay, lambda: self.do_on_change() 49 | ) 50 | 51 | def do_on_change(self) -> None: 52 | if not self.widget.winfo_exists(): 53 | return 54 | 55 | text = self.widget.change_value() 56 | 57 | if self.changes[-1] == text: 58 | return 59 | 60 | self.changes.append(text) 61 | 62 | if len(self.changes) > 50: 63 | self.changes = self.changes[-config.max_changes :] 64 | 65 | self.changes_index = len(self.changes) - 1 66 | -------------------------------------------------------------------------------- /meltdown/close.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | # Standard 4 | from typing import TYPE_CHECKING 5 | 6 | # Modules 7 | from .args import args 8 | from .display import display 9 | from .dialogs import Dialog, Commands 10 | from .utils import utils 11 | 12 | 13 | if TYPE_CHECKING: 14 | from .display import Tab 15 | 16 | 17 | class Close: 18 | def get_old_tabs(self) -> list[Tab]: 19 | ids = display.tab_ids() 20 | old_tabs = [] 21 | 22 | max_minutes = args.old_tabs_minutes 23 | max_date = utils.now() - (60 * max_minutes) 24 | 25 | for tab_id in ids: 26 | tabconvo = display.get_tab_convo(tab_id) 27 | 28 | if not tabconvo: 29 | continue 30 | 31 | if tabconvo.convo.last_modified < max_date: 32 | old_tabs.append(tabconvo.tab) 33 | 34 | return old_tabs 35 | 36 | def get_oldest_tab(self) -> Tab | None: 37 | ids = display.tab_ids() 38 | oldest_tab = None 39 | oldest_date = utils.now() 40 | 41 | for tab_id in ids: 42 | tabconvo = display.get_tab_convo(tab_id) 43 | 44 | if not tabconvo: 45 | continue 46 | 47 | if tabconvo.convo.last_modified < oldest_date: 48 | oldest_tab = tabconvo.tab 49 | oldest_date = tabconvo.convo.last_modified 50 | 51 | return oldest_tab 52 | 53 | def get_empty_tabs(self) -> list[Tab]: 54 | ids = display.tab_ids() 55 | empty_tabs = [] 56 | 57 | for tab_id in ids: 58 | tabconvo = display.get_tab_convo(tab_id) 59 | 60 | if not tabconvo: 61 | continue 62 | 63 | if not tabconvo.convo.items: 64 | empty_tabs.append(tabconvo.tab) 65 | 66 | return empty_tabs 67 | 68 | def get_left_tabs(self, tab_id: str) -> list[str]: 69 | tab_ids = display.tab_ids() 70 | index = display.index(tab_id) 71 | return tab_ids[:index] 72 | 73 | def get_right_tabs(self, tab_id: str) -> list[str]: 74 | tab_ids = display.tab_ids() 75 | index = display.index(tab_id) 76 | return tab_ids[index + 1 :] 77 | 78 | def get_other_tabs(self, tab_id: str) -> list[str]: 79 | ids = display.tab_ids() 80 | return [tid for tid in ids if tid != tab_id] 81 | 82 | def get_pin_tabs(self) -> list[str]: 83 | return [tab.tab_id for tab in display.get_pins()] 84 | 85 | def get_normal_tabs(self) -> list[str]: 86 | return [tab.tab_id for tab in display.get_normal()] 87 | 88 | def close( 89 | self, 90 | tab_id: str | None = None, 91 | force: bool = False, 92 | make_empty: bool = True, 93 | force_empty: bool = False, 94 | full: bool = True, 95 | ) -> None: 96 | from .keyboard import keyboard 97 | 98 | if display.num_tabs() == 0: 99 | return 100 | 101 | if not tab_id: 102 | tab_id = display.book.current() 103 | single = False 104 | else: 105 | single = True 106 | 107 | if not tab_id: 108 | return 109 | 110 | if not force: 111 | picked = display.get_picked() 112 | 113 | if picked: 114 | self.close_picked() 115 | return 116 | 117 | tab = display.get_tab(tab_id) 118 | 119 | if not tab: 120 | return 121 | 122 | if force_empty: 123 | if tab.mode == "ignore": 124 | force = True 125 | 126 | if display.tab_is_empty(tab_id): 127 | force = True 128 | 129 | def action() -> None: 130 | display.book.close(tab_id) 131 | display.update_current_tab() 132 | display.remove_tab(tab_id) 133 | 134 | if display.num_tabs() == 0: 135 | if make_empty: 136 | display.make_tab() 137 | 138 | if force: 139 | action() 140 | return 141 | 142 | empty_tabs = self.get_empty_tabs() 143 | old_tabs = self.get_old_tabs() 144 | 145 | if keyboard.shift: 146 | if empty_tabs: 147 | self.close_empty() 148 | 149 | return 150 | 151 | if keyboard.ctrl: 152 | if old_tabs: 153 | self.close_old() 154 | 155 | return 156 | 157 | cmds = Commands() 158 | 159 | if full and empty_tabs: 160 | cmds.add("Empty", lambda a: self.close_empty()) 161 | 162 | if full and old_tabs: 163 | cmds.add("Old", lambda a: self.close_old()) 164 | 165 | if full and self.get_pin_tabs(): 166 | cmds.add("Normal", lambda a: self.close_normal()) 167 | 168 | if self.get_other_tabs(tab_id): 169 | cmds.add("Others", lambda a: self.close_others(tab_id=tab_id)) 170 | 171 | if self.get_left_tabs(tab_id): 172 | cmds.add("Left", lambda a: self.close_left(tab_id=tab_id)) 173 | 174 | if self.get_right_tabs(tab_id): 175 | cmds.add("Right", lambda a: self.close_right(tab_id=tab_id)) 176 | 177 | if full: 178 | cmds.add("All", lambda a: self.close_all()) 179 | 180 | if not cmds: 181 | return 182 | 183 | cmds.add("Ok" if single else "One", lambda a: action()) 184 | msg = "Close tab ?" if single else "Close tabs ?" 185 | Dialog.show_dialog(msg, cmds) 186 | 187 | def close_all(self, force: bool = False, make_empty: bool = True) -> None: 188 | def action() -> None: 189 | for tab_id in display.tab_ids(): 190 | self.close(tab_id=tab_id, force=True, make_empty=make_empty) 191 | 192 | if force or (not args.confirm_close): 193 | action() 194 | return 195 | 196 | n = display.num_tabs() 197 | Dialog.show_confirm(f"Close all tabs ({n}) ?", lambda: action()) 198 | 199 | def close_old(self, force: bool = False) -> None: 200 | ids = display.tab_ids() 201 | 202 | if len(ids) <= 1: 203 | return 204 | 205 | tabs = self.get_old_tabs() 206 | 207 | if not tabs: 208 | return 209 | 210 | def action() -> None: 211 | for tab in tabs: 212 | self.close(tab_id=tab.tab_id, force=True, make_empty=True) 213 | 214 | if force or (not args.confirm_close): 215 | action() 216 | return 217 | 218 | n = len(tabs) 219 | Dialog.show_confirm(f"Close old tabs ({n}) ?", lambda: action()) 220 | 221 | def close_others(self, force: bool = False, tab_id: str | None = None) -> None: 222 | if not tab_id: 223 | tab_id = display.current_tab 224 | 225 | tab_ids = self.get_other_tabs(tab_id) 226 | 227 | def action() -> None: 228 | for tab_id in tab_ids: 229 | self.close(tab_id, force=True) 230 | 231 | if force or (not args.confirm_close): 232 | action() 233 | return 234 | 235 | n = len(tab_ids) 236 | Dialog.show_confirm(f"Close other tabs ({n}) ?", lambda: action()) 237 | 238 | def close_pins(self, force: bool = False, tab_id: str | None = None) -> None: 239 | if not tab_id: 240 | tab_id = display.current_tab 241 | 242 | tab_ids = self.get_pin_tabs() 243 | 244 | if not tab_ids: 245 | return 246 | 247 | def action() -> None: 248 | for tab_id in tab_ids: 249 | self.close(tab_id=tab_id, force=True) 250 | 251 | if force or (not args.confirm_close): 252 | action() 253 | return 254 | 255 | n = len(tab_ids) 256 | Dialog.show_confirm(f"Close pinned tabs ({n}) ?", lambda: action()) 257 | 258 | def close_normal(self, force: bool = False, tab_id: str | None = None) -> None: 259 | if not tab_id: 260 | tab_id = display.current_tab 261 | 262 | tab_ids = self.get_normal_tabs() 263 | 264 | if not tab_ids: 265 | return 266 | 267 | def action() -> None: 268 | for tab_id in tab_ids: 269 | self.close(tab_id=tab_id, force=True) 270 | 271 | if force or (not args.confirm_close): 272 | action() 273 | return 274 | 275 | n = len(tab_ids) 276 | Dialog.show_confirm(f"Close normal tabs ({n}) ?", lambda: action()) 277 | 278 | def close_left(self, force: bool = False, tab_id: str | None = None) -> None: 279 | if not tab_id: 280 | tab_id = display.current_tab 281 | 282 | tab_ids = self.get_left_tabs(tab_id) 283 | 284 | if not tab_ids: 285 | return 286 | 287 | def action() -> None: 288 | for tab_id in tab_ids: 289 | self.close(tab_id=tab_id, force=True) 290 | 291 | if force or (not args.confirm_close): 292 | action() 293 | return 294 | 295 | n = len(tab_ids) 296 | Dialog.show_confirm(f"Close tabs to the left ({n}) ?", lambda: action()) 297 | 298 | def close_right(self, force: bool = False, tab_id: str | None = None) -> None: 299 | if not tab_id: 300 | tab_id = display.current_tab 301 | 302 | tab_ids = self.get_right_tabs(tab_id) 303 | 304 | if not tab_ids: 305 | return 306 | 307 | def action() -> None: 308 | for tab_id in tab_ids: 309 | self.close(tab_id=tab_id, force=True) 310 | 311 | if force or (not args.confirm_close): 312 | action() 313 | return 314 | 315 | n = len(tab_ids) 316 | Dialog.show_confirm(f"Close tabs to the right ({n}) ?", lambda: action()) 317 | 318 | def close_empty(self, force: bool = False) -> None: 319 | ids = display.tab_ids() 320 | 321 | if len(ids) <= 1: 322 | return 323 | 324 | tabs = self.get_empty_tabs() 325 | 326 | if not tabs: 327 | return 328 | 329 | def action() -> None: 330 | for tab in tabs: 331 | self.close(tab_id=tab.tab_id, force=True, make_empty=True) 332 | 333 | if force or (not args.confirm_close): 334 | action() 335 | return 336 | 337 | n = len(tabs) 338 | Dialog.show_confirm(f"Close empty tabs ({n}) ?", lambda: action()) 339 | 340 | def close_picked(self, force: bool = False) -> None: 341 | tabs = display.get_picked() 342 | 343 | if not tabs: 344 | return 345 | 346 | def action() -> None: 347 | for tab in tabs: 348 | self.close(tab_id=tab.tab_id, force=True, make_empty=True) 349 | 350 | display.unpick() 351 | 352 | if force or (not args.confirm_close): 353 | action() 354 | return 355 | 356 | n = len(tabs) 357 | Dialog.show_confirm(f"Close picked tabs ({n}) ?", lambda: action()) 358 | 359 | def close_oldest(self, force: bool = False) -> None: 360 | tab = self.get_oldest_tab() 361 | 362 | if not tab: 363 | return 364 | 365 | def action() -> None: 366 | self.close(tab_id=tab.tab_id, force=True, make_empty=True) 367 | 368 | if force: 369 | action() 370 | else: 371 | Dialog.show_confirm("Close oldest tab ?", lambda: action()) 372 | 373 | 374 | close = Close() 375 | -------------------------------------------------------------------------------- /meltdown/commands.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | # Standard 4 | import re 5 | from typing import Any 6 | from pathlib import Path 7 | from dataclasses import dataclass 8 | 9 | # Modules 10 | from .app import app 11 | from .dialogs import Dialog 12 | from .menus import Menu 13 | from .args import args 14 | from .paths import paths 15 | from .utils import utils 16 | from .files import files 17 | 18 | 19 | @dataclass 20 | class QueueItem: 21 | def __init__(self, cmd: str, argument: str) -> None: 22 | self.cmd = cmd 23 | self.argument = argument 24 | 25 | 26 | @dataclass 27 | class Queue: 28 | def __init__(self, items: list[QueueItem], wait: float = 0.0) -> None: 29 | self.items = items 30 | self.wait = wait 31 | 32 | 33 | class Commands: 34 | def __init__(self) -> None: 35 | self.commands: dict[str, dict[str, Any]] = {} 36 | self.loop_delay = 25 37 | self.queues: list[Queue] = [] 38 | self.aliases: dict[str, str] = {} 39 | 40 | def setup(self) -> None: 41 | prefix = utils.escape_regex(args.command_prefix) 42 | andchar = utils.escape_regex(args.andchar) 43 | self.cmd_pattern = rf"{andchar}(?= {prefix}\w+)" 44 | 45 | self.make_commands() 46 | self.make_aliases() 47 | self.load_file() 48 | self.start_loop() 49 | self.get_cmdkeys() 50 | 51 | def get_cmdkeys(self) -> None: 52 | self.cmdkeys = [] 53 | 54 | for key in self.commands: 55 | self.cmdkeys.append(key) 56 | 57 | for key in self.aliases: 58 | self.cmdkeys.append(key) 59 | 60 | def start_loop(self) -> None: 61 | def loop() -> None: 62 | to_remove = [] 63 | 64 | for queue in self.queues: 65 | if queue.wait: 66 | queue.wait -= self.loop_delay 67 | 68 | if queue.wait <= 0.0: 69 | queue.wait = 0.0 70 | 71 | continue 72 | 73 | if queue.items: 74 | item = queue.items.pop(0) 75 | 76 | if item.cmd == "sleep": 77 | if not item.argument: 78 | item.argument = "1" 79 | 80 | if item.argument and queue.items: 81 | queue.wait = float(item.argument) * 1000.0 82 | elif self.aliases.get(item.cmd): 83 | self.exec(self.aliases[item.cmd], queue) 84 | elif not self.try_to_run(item.cmd, item.argument): 85 | similar = self.get_similar_alias(item.cmd) 86 | 87 | if similar: 88 | self.exec(self.aliases[similar], queue) 89 | 90 | if not queue.items: 91 | to_remove.append(queue) 92 | 93 | for rm_item in to_remove: 94 | self.queues.remove(rm_item) 95 | 96 | app.root.after(self.loop_delay, lambda: loop()) 97 | 98 | loop() 99 | 100 | def make_commands(self) -> None: 101 | from .command_spec import CommandSpec 102 | 103 | command_spec = CommandSpec() 104 | self.commands = command_spec.commands 105 | 106 | for key in self.commands: 107 | self.commands[key]["date"] = 0.0 108 | 109 | def make_aliases(self) -> None: 110 | for alias in args.aliases: 111 | key, value = utils.cmd_value(alias) 112 | 113 | if (not key) or (not value): 114 | continue 115 | 116 | self.aliases[key] = value 117 | 118 | def set_alias(self, cmd: str) -> None: 119 | from .display import display 120 | 121 | def fmt() -> None: 122 | display.print("Format: [name] [value]") 123 | 124 | if " " not in cmd: 125 | fmt() 126 | return 127 | 128 | name, value = utils.cmd_value(cmd) 129 | 130 | if (not name) or (not value): 131 | fmt() 132 | return 133 | 134 | self.aliases[name] = value 135 | prefix = args.command_prefix 136 | display.print(f"Set Alias: `{prefix}{name}` is now `{value}`", do_format=True) 137 | 138 | def unset_alias(self, name: str) -> None: 139 | from .display import display 140 | 141 | if name in self.aliases: 142 | del self.aliases[name] 143 | display.print(f"Unset Alias: {name}") 144 | else: 145 | display.print(f"Alias not found: {name}") 146 | 147 | def read_alias(self, name: str) -> None: 148 | from .display import display 149 | 150 | if name in self.aliases: 151 | display.print(f"Alias: `{name}` is `{self.aliases[name]}`", do_format=True) 152 | else: 153 | display.print(f"Alias not found: {name}") 154 | 155 | def is_command(self, text: str) -> bool: 156 | if len(text) < 2: 157 | return False 158 | 159 | if "\n" in text: 160 | return False 161 | 162 | with_prefix = text.startswith(args.command_prefix) 163 | second_char = text[1:2] 164 | return with_prefix and second_char.isalpha() 165 | 166 | def exec(self, text: str, queue: Queue | None = None) -> bool: 167 | text = text.strip() 168 | 169 | if not text: 170 | return False 171 | 172 | if not self.is_command(text): 173 | return False 174 | 175 | cmds = re.split(self.cmd_pattern, text) 176 | items = [] 177 | 178 | for item in cmds: 179 | split = item.strip().split(" ") 180 | cmd = split[0][1:] 181 | argument = " ".join(split[1:]) 182 | queue_item = QueueItem(cmd, argument) 183 | items.append(queue_item) 184 | 185 | if items: 186 | if queue: 187 | queue.items = items + queue.items 188 | else: 189 | queue = Queue(items) 190 | self.queues.append(queue) 191 | 192 | return True 193 | 194 | def run( 195 | self, cmd: str, argument: str | None = None, update_date: bool = False 196 | ) -> None: 197 | item = self.commands.get(cmd) 198 | 199 | if not item: 200 | return 201 | 202 | arg_req = item.get("arg_req") 203 | 204 | if arg_req and (argument in [None, ""]): 205 | return 206 | 207 | argtype = item.get("type") 208 | new_argument: Any = None 209 | 210 | if argtype: 211 | if argtype == "force": 212 | if argument: 213 | new_argument = argument.lower() == "force" 214 | else: 215 | new_argument = False 216 | elif argument and (argtype is str): 217 | new_argument = utils.replace_keywords(argument) 218 | elif argument: 219 | try: 220 | new_argument = argtype(argument) 221 | except ValueError: 222 | return 223 | 224 | item = self.commands[cmd] 225 | item["action"](new_argument) 226 | 227 | if update_date: 228 | item["date"] = utils.now() 229 | 230 | self.save_commands() 231 | 232 | def save_commands(self) -> None: 233 | cmds = {} 234 | 235 | for key in self.commands: 236 | cmds[key] = {"date": self.commands[key]["date"]} 237 | 238 | files.save(paths.commands, cmds) 239 | 240 | def try_to_run(self, cmd: str, argument: str) -> bool: 241 | for key in self.commands: 242 | if cmd == key: 243 | self.run(key, argument) 244 | return True 245 | 246 | most_similar = utils.most_similar(cmd, list(self.commands.keys())) 247 | 248 | if most_similar: 249 | self.run(most_similar, argument) 250 | return True 251 | 252 | return False 253 | 254 | def get_similar_alias(self, cmd: str) -> str | None: 255 | return utils.most_similar(cmd, list(self.aliases.keys())) 256 | 257 | def help(self) -> None: 258 | from .model import model 259 | 260 | text = utils.replace_keywords(args.help_prompt) 261 | prompt = {"text": text} 262 | model.stream(prompt) 263 | 264 | def show_help( 265 | self, tab_id: str | None = None, filter_text: str | None = None 266 | ) -> None: 267 | from .display import display 268 | 269 | text = self.get_commandtext(filter_text) 270 | display.print(text, tab_id=tab_id) 271 | display.format_text(tab_id=tab_id, mode="all") 272 | 273 | def make_palette(self) -> None: 274 | self.palette = Menu() 275 | 276 | def add_item(key: str) -> None: 277 | cmd = self.commands[key] 278 | 279 | if cmd.get("skip_palette"): 280 | return 281 | 282 | def command() -> None: 283 | if cmd.get("arg_req"): 284 | Dialog.show_input("Argument", lambda a: self.run(key, a)) 285 | else: 286 | self.run(key, update_date=True) 287 | 288 | if args.alt_palette: 289 | text = key 290 | tooltip = cmd["info"] 291 | else: 292 | text = cmd["info"] 293 | tooltip = key 294 | 295 | self.palette.add(text=text, command=lambda e: command(), tooltip=tooltip) 296 | 297 | keys = sorted( 298 | self.commands, key=lambda x: self.commands[x]["date"], reverse=True 299 | ) 300 | 301 | for key in keys: 302 | add_item(key) 303 | 304 | def show_palette(self) -> None: 305 | from .widgets import widgets 306 | 307 | self.make_palette() 308 | self.palette.show(widget=widgets.main_menu_button) 309 | 310 | def cmd(self, text: str) -> str: 311 | return args.command_prefix + text 312 | 313 | def load_file(self) -> None: 314 | if (not paths.commands.exists()) or (not paths.commands.is_file()): 315 | return 316 | 317 | try: 318 | cmds = files.load(paths.commands) 319 | except BaseException as e: 320 | utils.error(e) 321 | cmds = {} 322 | 323 | for key in cmds: 324 | if key in self.commands: 325 | self.commands[key]["date"] = cmds[key].get("date", 0.0) 326 | 327 | def get_commandtext(self, filter_text: str | None = None) -> str: 328 | sep = "\n\n---\n\n" 329 | text = "" 330 | 331 | if not filter_text: 332 | text += "# Commands\n\n" 333 | text += "Commands can be chained:\n\n" 334 | 335 | text += "```\n" 336 | text += "/tab 2 & /sleep 0.5 & /select\n" 337 | text += "```\n\n" 338 | 339 | text += "This will select tab 2, then wait 500ms, then select all.\n\n" 340 | 341 | text += "Here are all the available commands:" 342 | 343 | for key in self.commands: 344 | cmd = self.commands[key] 345 | txt = "" 346 | txt += sep 347 | txt += f"### {key}\n\n" 348 | txt += cmd["info"] 349 | extra = cmd.get("extra") 350 | 351 | if extra: 352 | txt += f"\n\n{extra}" 353 | 354 | if filter_text: 355 | if filter_text.lower() not in txt.lower(): 356 | continue 357 | 358 | text += txt 359 | 360 | text += "\n" 361 | return text.lstrip() 362 | 363 | def make_commandoc(self, pathstr: str) -> None: 364 | from .display import display 365 | 366 | path = Path(pathstr) 367 | 368 | if (not path.parent.exists()) or (not path.parent.is_dir()): 369 | utils.msg(f"Invalid path: {pathstr}") 370 | return 371 | 372 | text = self.get_commandtext() 373 | files.write(path, text) 374 | msg = f"Saved to {path}" 375 | display.print(msg) 376 | utils.msg(msg) 377 | 378 | def after_stream(self) -> None: 379 | if args.after_stream: 380 | app.root.after(100, lambda: self.exec(args.after_stream)) 381 | 382 | 383 | commands = Commands() 384 | -------------------------------------------------------------------------------- /meltdown/console.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | # Standard 4 | import threading 5 | from typing import Any 6 | from collections.abc import Generator 7 | 8 | # Libraries 9 | from prompt_toolkit import PromptSession # type: ignore 10 | from prompt_toolkit.history import InMemoryHistory # type: ignore 11 | from prompt_toolkit.completion import Completer, Completion # type: ignore 12 | from prompt_toolkit.document import Document # type: ignore 13 | from prompt_toolkit.key_binding import KeyBindings # type: ignore 14 | 15 | # Modules 16 | from .args import args 17 | from .app import app 18 | from .commands import commands 19 | from .inputcontrol import inputcontrol 20 | from .utils import utils 21 | 22 | 23 | completer: Completer 24 | words: list[str] = [] 25 | 26 | 27 | class SlashCompleter(Completer): # type: ignore 28 | def get_completions( 29 | self, document: Document, complete_event: Any 30 | ) -> Generator[Completion, None, None]: 31 | text = document.get_word_before_cursor(WORD=True).strip() 32 | 33 | if not text: 34 | return 35 | 36 | if text.startswith(args.command_prefix): 37 | for word in words: 38 | if word.startswith(text): 39 | yield Completion(word, start_position=-len(text)) 40 | else: 41 | for word in words: 42 | if word.startswith(text): 43 | yield Completion(word, start_position=-len(text)) 44 | 45 | 46 | class Console: 47 | def __init__(self) -> None: 48 | self.session: PromptSession[Any] | None = None 49 | 50 | def add_word(self, word: str) -> None: 51 | if word not in words: 52 | words.append(word) 53 | 54 | def start(self) -> None: 55 | if not args.console: 56 | return 57 | 58 | thread = threading.Thread(target=lambda: self.do_start()) 59 | thread.daemon = True 60 | thread.start() 61 | 62 | def do_start(self) -> None: 63 | kb = KeyBindings() 64 | 65 | @kb.add("c-v") # type: ignore 66 | def _(event: Any) -> None: 67 | clipboard_data = utils.get_paste() 68 | 69 | if not clipboard_data: 70 | return 71 | 72 | event.current_buffer.insert_text(clipboard_data) 73 | 74 | history = InMemoryHistory() 75 | 76 | words.extend([f"/{key}" for key in commands.cmdkeys]) 77 | words.extend(inputcontrol.autocomplete) 78 | 79 | completer = SlashCompleter() 80 | 81 | self.session = PromptSession( 82 | history=history, 83 | completer=completer, 84 | reserve_space_for_menu=args.console_height, 85 | vi_mode=args.console_vi, 86 | key_bindings=kb, 87 | ) 88 | 89 | while True: 90 | try: 91 | text = self.session.prompt("Input: ") 92 | except KeyboardInterrupt: 93 | app.destroy() 94 | return 95 | except Exception: 96 | continue 97 | 98 | if not text: 99 | continue 100 | 101 | inputcontrol.submit(text=text) 102 | 103 | 104 | console = Console() 105 | -------------------------------------------------------------------------------- /meltdown/contrast_theme.py: -------------------------------------------------------------------------------- 1 | # Modules 2 | from .theme import Theme 3 | 4 | 5 | class ContrastTheme(Theme): 6 | def __init__(self) -> None: 7 | super().__init__() 8 | self.background_color = "black" 9 | self.foreground_color = "white" 10 | 11 | self.output_background = "black" 12 | self.output_foreground = "white" 13 | 14 | self.entry_background = "black" 15 | self.entry_foreground = "white" 16 | self.entry_placeholder_color = "#494D62" 17 | self.entry_insert = "white" 18 | self.entry_border_width = 1 19 | 20 | self.entry_background_dialog = "black" 21 | self.entry_foreground_dialog = "white" 22 | self.entry_insert_dialog = "white" 23 | self.entry_selection_background_dialog = "white" 24 | self.entry_selection_foreground_dialog = "black" 25 | self.entry_border_width_dialog = 1 26 | 27 | self.combobox_background = "black" 28 | self.combobox_foreground = "white" 29 | self.combobox_border_width = 1 30 | 31 | self.button_background = "#1ae6e6" 32 | self.button_foreground = "black" 33 | self.button_hover_background = "#1ae6e6" 34 | 35 | self.button_alt_background = "#3fbebe" 36 | self.button_alt_hover_background = "#38f1f1" 37 | 38 | self.button_highlight_background = "#ffff00" 39 | self.button_highlight_hover_background = "#ffff00" 40 | 41 | self.button_active_background = "#ffff00" 42 | self.button_active_hover_background = "#ffff00" 43 | 44 | self.button_disabled_background = "#2B303B" 45 | 46 | self.tab_normal_background = "black" 47 | self.tab_normal_foreground = "white" 48 | self.tab_selected_background = "white" 49 | self.tab_selected_foreground = "black" 50 | self.tab_border = "white" 51 | self.tabs_container_color = "black" 52 | self.tab_picked_border = "#ff0000" 53 | 54 | self.system_normal = "#1ae6e6" 55 | self.system_heavy = "#FF6B6B" 56 | 57 | self.dialog_background = "black" 58 | self.dialog_foreground = "white" 59 | self.dialog_border = "white" 60 | self.dialog_border_width = 3 61 | self.dialog_top_frame = "black" 62 | 63 | self.snippet_background = "black" 64 | self.snippet_foreground = "white" 65 | 66 | self.snippet_header_background = "white" 67 | self.snippet_header_foreground = "black" 68 | 69 | self.snippet_selection_background = "#C3C3C3" 70 | self.snippet_selection_foreground = "black" 71 | 72 | self.menu_background = "black" 73 | self.menu_foreground = "white" 74 | self.menu_hover_background = "white" 75 | self.menu_hover_foreground = "black" 76 | self.menu_disabled_background = "black" 77 | self.menu_disabled_foreground = "white" 78 | self.menu_border = "white" 79 | self.menu_canvas_background = "black" 80 | 81 | self.separator_color = "white" 82 | 83 | self.textbox_background = "black" 84 | self.textbox_foreground = "white" 85 | self.textbox_insert = "white" 86 | 87 | self.effect_color = "#1d99ff" 88 | -------------------------------------------------------------------------------- /meltdown/dark_theme.py: -------------------------------------------------------------------------------- 1 | # Modules 2 | from .theme import Theme 3 | 4 | 5 | class DarkTheme(Theme): 6 | def __init__(self) -> None: 7 | super().__init__() 8 | -------------------------------------------------------------------------------- /meltdown/delete.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | # Modules 4 | from .args import args 5 | from .utils import utils 6 | from .dialogs import Dialog 7 | 8 | 9 | class Delete: 10 | def delete_items( 11 | self, 12 | number: str | None = None, 13 | tab_id: str | None = None, 14 | mode: str = "normal", 15 | force: bool = False, 16 | ) -> None: 17 | from .display import display 18 | from .session import session 19 | 20 | tabconvo = display.get_tab_convo(tab_id) 21 | 22 | if not tabconvo: 23 | return 24 | 25 | if not tabconvo.convo.items: 26 | return 27 | 28 | if tabconvo.convo.id == "ignore": 29 | return 30 | 31 | if (mode == "above") or (mode == "below") or (mode == "others"): 32 | if len(tabconvo.convo.items) <= 1: 33 | return 34 | 35 | def check_index(index: int) -> bool: 36 | if not tabconvo: 37 | return False 38 | 39 | if index < 0: 40 | return False 41 | 42 | return index < len(tabconvo.convo.items) 43 | 44 | if not number: 45 | number = "last" 46 | 47 | index = utils.get_index(number, tabconvo.convo.items) 48 | 49 | if not check_index(index): 50 | return 51 | 52 | def action() -> None: 53 | if not tabconvo: 54 | return 55 | 56 | if not tabconvo.tab.tab_id: 57 | return 58 | 59 | if mode == "normal": 60 | tabconvo.convo.items.pop(index) 61 | elif mode == "above": 62 | tabconvo.convo.items = tabconvo.convo.items[index:] 63 | elif mode == "below": 64 | tabconvo.convo.items = tabconvo.convo.items[: index + 1] 65 | elif mode == "others": 66 | tabconvo.convo.items = [tabconvo.convo.items[index]] 67 | 68 | session.save() 69 | display.reset_tab(tabconvo.tab) 70 | 71 | if tabconvo.convo.items: 72 | tabconvo.convo.print() 73 | 74 | if not args.confirm_delete: 75 | force = True 76 | 77 | if force: 78 | action() 79 | return 80 | 81 | if mode == "normal": 82 | n = 1 83 | elif mode == "above": 84 | n = index 85 | elif mode == "below": 86 | n = len(tabconvo.convo.items) - index - 1 87 | elif mode == "others": 88 | n = len(tabconvo.convo.items) - 1 89 | else: 90 | return 91 | 92 | Dialog.show_confirm(f"Delete items ({n}) ?", lambda: action()) 93 | 94 | 95 | delete = Delete() 96 | -------------------------------------------------------------------------------- /meltdown/details.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | # Standard 4 | from typing import TYPE_CHECKING 5 | 6 | # Modules 7 | from .app import app 8 | from .tooltips import ToolTip 9 | from .tips import tips 10 | from .utils import utils 11 | from .widgetutils import widgetutils 12 | 13 | # Try Import 14 | llama_cpp = utils.try_import("llama_cpp") 15 | 16 | if llama_cpp: 17 | Formats = llama_cpp.llama_chat_format.LlamaChatCompletionHandlerRegistry 18 | 19 | 20 | if TYPE_CHECKING: 21 | from .widgets import Widgets 22 | from .framedata import FrameData 23 | 24 | 25 | class Details: 26 | def __init__(self) -> None: 27 | self.width_1 = 10 28 | 29 | def make_label( 30 | self, 31 | widgets: Widgets, 32 | data: FrameData, 33 | key: str, 34 | label: str, 35 | padx: tuple[int, int] | None = None, 36 | ) -> None: 37 | label_wid = widgetutils.make_label(data, label, padx=padx) 38 | setattr(widgets, f"{key}_label", label_wid) 39 | ToolTip(label_wid, tips[key]) 40 | 41 | def make_entry( 42 | self, 43 | widgets: Widgets, 44 | data: FrameData, 45 | key: str, 46 | width: int | None = None, 47 | ) -> None: 48 | width = width or app.theme.entry_width_small 49 | entry_wid = widgetutils.make_entry(data, width=width) 50 | setattr(widgets, key, entry_wid) 51 | ToolTip(entry_wid, tips[key]) 52 | 53 | def make_combobox( 54 | self, 55 | widgets: Widgets, 56 | data: FrameData, 57 | key: str, 58 | values: list[str], 59 | width: int | None = None, 60 | ) -> None: 61 | width = width or 15 62 | combo_wid = widgetutils.make_combobox(data, values=values, width=width) 63 | setattr(widgets, key, combo_wid) 64 | ToolTip(combo_wid, tips[key]) 65 | 66 | def add_users(self, widgets: Widgets, data: FrameData) -> None: 67 | self.make_label(widgets, data, "user", "User", padx=(0, app.theme.padx)) 68 | self.make_entry(widgets, data, "avatar_user", width=4) 69 | self.make_entry(widgets, data, "name_user", width=self.width_1) 70 | 71 | self.make_label(widgets, data, "ai", "AI") 72 | self.make_entry(widgets, data, "avatar_ai", width=4) 73 | self.make_entry(widgets, data, "name_ai", width=self.width_1) 74 | 75 | def add_history(self, widgets: Widgets, data: FrameData) -> None: 76 | self.make_label(widgets, data, "history", "History") 77 | self.make_entry(widgets, data, "history") 78 | 79 | def add_context(self, widgets: Widgets, data: FrameData) -> None: 80 | self.make_label(widgets, data, "context", "Context") 81 | self.make_entry(widgets, data, "context") 82 | 83 | def add_max_tokens(self, widgets: Widgets, data: FrameData) -> None: 84 | self.make_label(widgets, data, "max_tokens", "Tokens") 85 | self.make_entry(widgets, data, "max_tokens") 86 | 87 | def add_threads(self, widgets: Widgets, data: FrameData) -> None: 88 | self.make_label(widgets, data, "threads", "Threads") 89 | self.make_entry(widgets, data, "threads") 90 | 91 | def add_gpu_layers(self, widgets: Widgets, data: FrameData) -> None: 92 | self.make_label(widgets, data, "gpu_layers", "GPU") 93 | self.make_entry(widgets, data, "gpu_layers") 94 | 95 | def add_format(self, widgets: Widgets, data: FrameData) -> None: 96 | self.make_label(widgets, data, "format", "Format", padx=(0, app.theme.padx)) 97 | values = ["auto"] 98 | 99 | if llama_cpp: 100 | fmts = sorted(Formats._chat_handlers) 101 | values.extend(fmts) 102 | 103 | self.make_combobox(widgets, data, "format", values, width=13) 104 | 105 | def add_temperature(self, widgets: Widgets, data: FrameData) -> None: 106 | self.make_label(widgets, data, "temperature", "Temp") 107 | self.make_entry(widgets, data, "temperature") 108 | 109 | def add_logits(self, widgets: Widgets, data: FrameData) -> None: 110 | self.make_label(widgets, data, "logits", "Logits") 111 | self.make_combobox(widgets, data, "logits", ["normal", "all"], width=8) 112 | 113 | def add_seed(self, widgets: Widgets, data: FrameData) -> None: 114 | self.make_label(widgets, data, "seed", "Seed") 115 | self.make_entry(widgets, data, "seed") 116 | 117 | def add_top_p(self, widgets: Widgets, data: FrameData) -> None: 118 | self.make_label(widgets, data, "top_p", "Top P") 119 | self.make_entry(widgets, data, "top_p") 120 | 121 | def add_top_k(self, widgets: Widgets, data: FrameData) -> None: 122 | self.make_label(widgets, data, "top_k", "Top K") 123 | self.make_entry(widgets, data, "top_k") 124 | 125 | def add_before(self, widgets: Widgets, data: FrameData) -> None: 126 | self.make_label(widgets, data, "before", "Before") 127 | self.make_entry(widgets, data, "before", width=self.width_1) 128 | 129 | def add_after(self, widgets: Widgets, data: FrameData) -> None: 130 | self.make_label(widgets, data, "after", "After") 131 | self.make_entry(widgets, data, "after", width=self.width_1) 132 | 133 | def add_stop(self, widgets: Widgets, data: FrameData) -> None: 134 | self.make_label(widgets, data, "stop", "Stop") 135 | self.make_entry(widgets, data, "stop", width=self.width_1) 136 | 137 | def add_mlock(self, widgets: Widgets, data: FrameData) -> None: 138 | self.make_label(widgets, data, "mlock", "M-Lock") 139 | self.make_combobox(widgets, data, "mlock", ["yes", "no"], width=7) 140 | 141 | def add_items(self) -> None: 142 | from .framedata import FrameData 143 | from .widgets import widgets 144 | 145 | # Details 1 Items 146 | data = FrameData(widgets.scroller_details_1) 147 | self.add_users(widgets, data) 148 | self.add_history(widgets, data) 149 | self.add_context(widgets, data) 150 | self.add_max_tokens(widgets, data) 151 | self.add_temperature(widgets, data) 152 | self.add_threads(widgets, data) 153 | self.add_gpu_layers(widgets, data) 154 | 155 | # Details 2 Items 156 | data = FrameData(widgets.scroller_details_2) 157 | self.add_format(widgets, data) 158 | self.add_before(widgets, data) 159 | self.add_after(widgets, data) 160 | self.add_seed(widgets, data) 161 | self.add_top_p(widgets, data) 162 | self.add_top_k(widgets, data) 163 | self.add_stop(widgets, data) 164 | self.add_mlock(widgets, data) 165 | self.add_logits(widgets, data) 166 | 167 | 168 | details = Details() 169 | -------------------------------------------------------------------------------- /meltdown/entrybox.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | # Standard 4 | import re 5 | import tkinter as tk 6 | from tkinter import ttk 7 | from typing import Any 8 | from collections.abc import Callable 9 | 10 | # Modules 11 | from .app import app 12 | from .config import config 13 | from .tooltips import ToolTip 14 | 15 | 16 | class EntryBox(ttk.Entry): 17 | def __init__(self, *args: Any, mode: str = "normal", **kwargs: Any) -> None: 18 | from .changes import Changes 19 | 20 | if mode == "password": 21 | super().__init__(*args, show="*", **kwargs) 22 | else: 23 | super().__init__(*args, **kwargs) 24 | 25 | self.configure(font=app.theme.font("entry")) 26 | self.focused = False 27 | self.placeholder = "" 28 | self.key = "" 29 | self.text_var = tk.StringVar() 30 | self.text_var.set("") 31 | self.trace_id = self.text_var.trace_add("write", self.on_write) 32 | self.configure(textvariable=self.text_var) 33 | self.placeholder_active = False 34 | self.proc: Callable[..., Any] | None = None 35 | self.name = "" 36 | self.last_text = "" 37 | self.do_binds() 38 | self.changes = Changes(self) 39 | 40 | def do_binds(self) -> None: 41 | self.bind("", lambda e: self.on_focus_change("in")) 42 | self.bind("", lambda e: self.on_focus_change("out")) 43 | self.bind("", lambda e: self.select_all()) 44 | self.bind("", lambda e: self.on_left()) 45 | self.bind("", lambda e: self.on_right()) 46 | 47 | def bind_mousewheel(self) -> None: 48 | self.bind("", lambda e: self.on_mousewheel("up")) 49 | self.bind("", lambda e: self.on_mousewheel("down")) 50 | 51 | def on_mousewheel(self, direction: str) -> None: 52 | units = 2 53 | 54 | if direction == "up": 55 | self.xview_scroll(-units, "units") 56 | elif direction == "down": 57 | self.xview_scroll(units, "units") 58 | 59 | ToolTip.hide_all() 60 | 61 | def set_name(self, name: str) -> None: 62 | self.name = name 63 | 64 | def get(self) -> str: 65 | if self.placeholder_active: 66 | return "" 67 | 68 | return self.text_var.get() 69 | 70 | def get_text(self) -> str: 71 | return self.get() 72 | 73 | def get_selected(self) -> str | None: 74 | try: 75 | start = self.index(tk.SEL_FIRST) 76 | end = self.index(tk.SEL_LAST) 77 | return self.text_var.get()[start:end] 78 | except tk.TclError: 79 | return None 80 | 81 | def change_value(self) -> str: 82 | return self.get() 83 | 84 | def clear(self, focus: bool = True) -> None: 85 | self.placeholder_active = False 86 | self.set_text("") 87 | 88 | if focus: 89 | self.focus_set() 90 | 91 | def focus_end(self) -> None: 92 | self.focus_set() 93 | self.move_to_end() 94 | 95 | def focus_start(self) -> None: 96 | self.focus_set() 97 | self.move_to_start() 98 | 99 | def move_to_start(self) -> None: 100 | self.icursor(0) 101 | self.xview_moveto(0.0) 102 | 103 | def move_to_end(self) -> None: 104 | self.icursor(tk.END) 105 | self.xview_moveto(1.0) 106 | 107 | def select_all(self) -> None: 108 | def do_select() -> None: 109 | self.selection_range(0, tk.END) 110 | self.icursor(tk.END) 111 | 112 | self.after_idle(lambda: do_select()) 113 | 114 | def deselect_all(self) -> None: 115 | self.selection_clear() 116 | 117 | def set_text( 118 | self, text: str, check_placeholder: bool = True, on_change: bool = True 119 | ) -> None: 120 | self.text_var.trace_remove("write", self.trace_id) 121 | self.delete(0, tk.END) 122 | 123 | if self.proc: 124 | text = self.proc(text) 125 | 126 | self.insert(0, self.get_clean_string(text)) 127 | self.trace_id = self.text_var.trace_add("write", self.on_write) 128 | 129 | if check_placeholder: 130 | self.check_placeholder() 131 | 132 | if on_change and (not self.placeholder_active): 133 | self.changes.on_change() 134 | 135 | def insert_text( 136 | self, 137 | text: str, 138 | check_placeholder: bool = True, 139 | index: int = -1, 140 | add_space: bool = False, 141 | ) -> None: 142 | if self.placeholder_active: 143 | self.disable_placeholder() 144 | 145 | insert_index = self.index(tk.INSERT) 146 | end_index = self.index(tk.END) 147 | 148 | if index < 0: 149 | index = insert_index 150 | 151 | at_end = end_index == index 152 | 153 | if at_end: 154 | if add_space: 155 | if text[-1] != " ": 156 | text += " " 157 | 158 | self.insert(index, text) 159 | 160 | if at_end: 161 | self.move_to_end() 162 | 163 | if check_placeholder: 164 | self.check_placeholder() 165 | 166 | if not self.placeholder_active: 167 | self.changes.on_change() 168 | 169 | def delete_text(self, start: int, chars: int) -> None: 170 | self.delete(start, (start + chars)) 171 | 172 | def on_focus_change(self, mode: str) -> None: 173 | if mode == "out": 174 | if self.key and (not self.placeholder_active): 175 | config.update(self.key) 176 | 177 | self.deselect_all() 178 | self.focused = False 179 | elif mode == "in": 180 | self.focused = True 181 | 182 | self.check_placeholder() 183 | 184 | def enable_placeholder(self) -> None: 185 | if self.placeholder_active: 186 | return 187 | 188 | self.placeholder_active = True 189 | self.set_text(self.placeholder, check_placeholder=False) 190 | self.configure(foreground=app.theme.entry_placeholder_color) 191 | 192 | def disable_placeholder(self) -> None: 193 | if not self.placeholder_active: 194 | return 195 | 196 | self.placeholder_active = False 197 | 198 | if self.text_var.get() == self.placeholder: 199 | self.set_text("", check_placeholder=False) 200 | 201 | self.configure(foreground=app.theme.entry_foreground) 202 | 203 | def check_placeholder(self) -> None: 204 | if not self.placeholder: 205 | return 206 | 207 | text = self.get_text() 208 | 209 | if self.focused: 210 | self.disable_placeholder() 211 | elif self.placeholder_active: 212 | if text != self.placeholder: 213 | self.disable_placeholder() 214 | elif not text: 215 | self.enable_placeholder() 216 | 217 | def on_write(self, *args: Any) -> None: 218 | pos = self.index(tk.INSERT) 219 | self.check_placeholder() 220 | self.clean_string() 221 | self.icursor(pos) 222 | self.xview(pos - 10) 223 | 224 | def get_clean_string(self, text: str) -> str: 225 | return re.sub(r"\n+", " ", text) 226 | 227 | def clean_string(self) -> None: 228 | if self.placeholder_active: 229 | return 230 | 231 | text = self.text_var.get() 232 | text = self.get_clean_string(text) 233 | self.set_text(text) 234 | 235 | def on_left(self) -> str: 236 | from .keyboard import keyboard 237 | 238 | if keyboard.shift: 239 | return "" 240 | 241 | if keyboard.ctrl: 242 | return "" 243 | 244 | if self.selection_present(): 245 | self.icursor(tk.SEL_FIRST) 246 | self.selection_clear() 247 | return "break" 248 | 249 | return "" 250 | 251 | def on_right(self) -> str: 252 | from .keyboard import keyboard 253 | 254 | if keyboard.shift: 255 | return "" 256 | 257 | if keyboard.ctrl: 258 | return "" 259 | 260 | if self.selection_present(): 261 | self.icursor(tk.SEL_LAST) 262 | self.selection_clear() 263 | return "break" 264 | 265 | return "" 266 | 267 | def set_proc(self, proc: Callable[..., Any]) -> None: 268 | self.proc = proc 269 | 270 | def smart_space(self) -> None: 271 | if self.placeholder_active: 272 | return 273 | 274 | text = self.text_var.get() 275 | 276 | if not text: 277 | return 278 | 279 | if text[-1] == " ": 280 | return 281 | 282 | self.insert_text(" ") 283 | 284 | def reveal(self) -> None: 285 | self.configure(show="") 286 | -------------------------------------------------------------------------------- /meltdown/filecontrol.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | # Standard 4 | from typing import Any 5 | from tkinter import filedialog 6 | 7 | # Modules 8 | from .app import app 9 | from .tooltips import ToolTip 10 | from .entrybox import EntryBox 11 | from .widgetutils import widgetutils 12 | from .tips import tips 13 | from .files import files 14 | 15 | 16 | class FileControl: 17 | def __init__(self) -> None: 18 | self.history_index = -1 19 | self.file: EntryBox 20 | 21 | def fill(self) -> None: 22 | from .widgets import widgets 23 | 24 | frame_data = widgets.frame_data_file 25 | self.file_label = widgetutils.make_label(frame_data, "File") 26 | self.file = widgetutils.make_entry(frame_data) 27 | frame_data.expand() 28 | widgets.file = self.file 29 | self.file.bind_mousewheel() 30 | ToolTip(self.file_label, tips["file"]) 31 | ToolTip(self.file, tips["file"]) 32 | 33 | self.recent_files_button = widgetutils.make_button( 34 | frame_data, "Recent", lambda: self.show_recent() 35 | ) 36 | 37 | ToolTip(self.recent_files_button, tips["recent_files_button"]) 38 | 39 | self.browse_file_button = widgetutils.make_button( 40 | frame_data, "Browse", lambda: self.browse() 41 | ) 42 | 43 | ToolTip(self.browse_file_button, tips["browse_file_button"]) 44 | 45 | self.open_file_button = widgetutils.make_button( 46 | frame_data, "Open", lambda: self.open_file() 47 | ) 48 | 49 | ToolTip(self.open_file_button, tips["open_file_button"]) 50 | 51 | def bind(self) -> None: 52 | self.file.bind("", lambda e: self.show_menu(e)) 53 | 54 | def browse(self) -> None: 55 | from .widgets import widgets 56 | 57 | file = filedialog.askopenfilename(initialdir=widgets.get_dir(None, "files")) 58 | 59 | if file: 60 | self.set(file) 61 | 62 | def set(self, text: str) -> None: 63 | self.file.set_text(text) 64 | self.file.move_to_end() 65 | 66 | def show_context(self, event: Any = None, only_items: bool = False) -> None: 67 | from .widgets import widgets 68 | 69 | widgets.show_menu_items( 70 | "file", 71 | "files", 72 | lambda m: self.set(m), 73 | event, 74 | only_items=only_items, 75 | alt_cmd=lambda m: self.forget(m, event), 76 | ) 77 | 78 | def forget(self, text: str, event: Any) -> None: 79 | files.remove_file(text) 80 | self.show_context(event) 81 | 82 | def show_recent(self) -> None: 83 | self.show_context(only_items=True) 84 | 85 | def show_menu(self, event: Any = None) -> None: 86 | self.show_context(event) 87 | 88 | def open_file(self) -> None: 89 | file = self.file.get() 90 | 91 | if not file: 92 | files.open_last_file() 93 | return 94 | 95 | app.open_generic(file) 96 | 97 | def apply_history(self, inputs: list[str]) -> None: 98 | text = inputs[self.history_index] 99 | self.set(text) 100 | self.file.focus_end() 101 | 102 | def get_history_list(self) -> list[str]: 103 | return files.get_list("files") 104 | 105 | def history_up(self) -> None: 106 | files = self.get_history_list() 107 | 108 | if not files: 109 | return 110 | 111 | if self.history_index == -1: 112 | self.history_index = 0 113 | else: 114 | if self.history_index == len(files) - 1: 115 | self.clear() 116 | return 117 | 118 | self.history_index = (self.history_index + 1) % len(files) 119 | 120 | self.apply_history(files) 121 | 122 | def history_down(self) -> None: 123 | files = self.get_history_list() 124 | 125 | if not files: 126 | return 127 | 128 | if self.history_index == -1: 129 | self.history_index = len(files) - 1 130 | else: 131 | if self.history_index == 0: 132 | self.clear() 133 | return 134 | 135 | self.history_index = (self.history_index - 1) % len(files) 136 | 137 | self.apply_history(files) 138 | 139 | def clear(self) -> None: 140 | self.file.clear() 141 | self.reset_history_index() 142 | 143 | def reset_history_index(self) -> None: 144 | self.history_index = -1 145 | 146 | 147 | filecontrol = FileControl() 148 | -------------------------------------------------------------------------------- /meltdown/files.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | # Standard 4 | import json 5 | from typing import Any 6 | from pathlib import Path 7 | 8 | # Modules 9 | from .app import app 10 | from .paths import paths 11 | from .config import config 12 | from .args import args 13 | 14 | 15 | class Files: 16 | def __init__(self) -> None: 17 | self.models_list: list[str] = [] 18 | self.inputs_list: list[str] = [] 19 | self.systems_list: list[str] = [] 20 | self.files_list: list[str] = [] 21 | 22 | self.models_loaded = False 23 | self.inputs_loaded = False 24 | self.systems_loaded = False 25 | self.files_loaded = False 26 | 27 | def save(self, path: Path, dictionary: Any) -> None: 28 | with path.open("w", encoding="utf-8") as file: 29 | json.dump(dictionary, file, indent=4) 30 | 31 | def load_list(self, key: str) -> None: 32 | path: Path = getattr(paths, key) 33 | 34 | if not path.exists(): 35 | path.parent.mkdir(parents=True, exist_ok=True) 36 | path.touch(exist_ok=True) 37 | 38 | name = f"{key}_list" 39 | 40 | try: 41 | items = self.load(path) 42 | except BaseException: 43 | items = [] 44 | 45 | if hasattr(self, name): 46 | item = getattr(self, name) 47 | 48 | if item: 49 | items.append(item) 50 | 51 | setattr(self, name, items) 52 | 53 | def add_model(self, text: str) -> None: 54 | self.add_to_list("models", text) 55 | 56 | def remove_model(self, text: str) -> None: 57 | self.remove_from_list("models", text) 58 | 59 | def add_input(self, text: str) -> None: 60 | self.add_to_list("inputs", text) 61 | 62 | def remove_input(self, text: str) -> None: 63 | self.remove_from_list("inputs", text) 64 | 65 | def add_system(self, text: str) -> None: 66 | if text == config.default_system: 67 | return 68 | 69 | self.add_to_list("systems", text) 70 | 71 | def remove_system(self, text: str) -> None: 72 | self.remove_from_list("systems", text) 73 | 74 | def add_file(self, text: str) -> None: 75 | self.add_to_list("files", text) 76 | 77 | def remove_file(self, text: str) -> None: 78 | self.remove_from_list("files", text) 79 | 80 | def add_to_list(self, key: str, text: str) -> None: 81 | if not text: 82 | return 83 | 84 | text = text[: args.list_item_max_length] 85 | 86 | if not getattr(self, f"{key}_loaded"): 87 | self.load_list(key) 88 | 89 | name = f"{key}_list" 90 | items = getattr(self, name) 91 | new_items = [item for item in items if item != text] 92 | new_items.insert(0, text) 93 | new_items = new_items[: config.max_file_list] 94 | setattr(self, name, new_items) 95 | path = getattr(paths, key) 96 | self.save(path, new_items) 97 | 98 | def remove_from_list(self, key: str, text: str) -> None: 99 | if not text: 100 | return 101 | 102 | if not getattr(self, f"{key}_loaded"): 103 | self.load_list(key) 104 | 105 | name = f"{key}_list" 106 | items = getattr(self, name) 107 | new_items = [item for item in items if item != text] 108 | setattr(self, name, new_items) 109 | path = getattr(paths, key) 110 | self.save(path, new_items) 111 | 112 | def get_list(self, what: str) -> list[str]: 113 | if not getattr(self, f"{what}_loaded"): 114 | self.load_list(what) 115 | 116 | lst = getattr(self, f"{what}_list") 117 | return lst or [] 118 | 119 | def open_last_file(self) -> None: 120 | if self.files_list: 121 | file = self.files_list[0] 122 | app.open_generic(file) 123 | 124 | def load(self, path: Path) -> Any: 125 | with path.open("r", encoding="utf-8") as file: 126 | return json.load(file) 127 | 128 | def read(self, path: Path) -> str: 129 | with path.open("r", encoding="utf-8") as file: 130 | return file.read().strip() 131 | 132 | def write(self, path: Path, text: str) -> None: 133 | with path.open("w", encoding="utf-8") as file: 134 | file.write(text) 135 | 136 | def clean_path(self, path: str) -> str: 137 | return path.replace("file://", "", 1) 138 | 139 | def full_name(self, name: str, ext: str = "json") -> str: 140 | if name.endswith(f".{ext}"): 141 | return name 142 | 143 | return f"{name}.{ext}" 144 | 145 | 146 | files = Files() 147 | -------------------------------------------------------------------------------- /meltdown/find.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | # Standard 4 | import re 5 | import tkinter as tk 6 | 7 | # Modules 8 | from .app import app 9 | from .buttonbox import ButtonBox 10 | from .entrybox import EntryBox 11 | from .tooltips import ToolTip 12 | from .output import Output 13 | from .tips import tips 14 | 15 | 16 | class Find: 17 | def __init__(self, parent: tk.Frame, tab_id: str) -> None: 18 | self.parent = parent 19 | self.tab_id = tab_id 20 | self.root = tk.Frame(parent) 21 | self.inner = tk.Frame(self.root) 22 | self.root.configure(background=app.theme.find_background) 23 | self.inner.configure(background=app.theme.find_background) 24 | w = app.theme.find_entry_width 25 | 26 | self.entry = EntryBox( 27 | self.inner, style="Normal.TEntry", font=app.theme.font(), width=w 28 | ) 29 | 30 | self.entry.set_name("find") 31 | ToolTip(self.entry, "Enter some text and hit Enter") 32 | self.entry.grid(row=0, column=0, sticky="ew", padx=4) 33 | self.entry.placeholder = "Find..." 34 | self.entry.check_placeholder() 35 | self.current_match: str | None = None 36 | self.current_match_reverse: str | None = None 37 | self.widget: tk.Text | None = None 38 | self.visible = False 39 | self.snippet = -1 40 | self.snippet_focused = False 41 | self.snippet_index = "1.0" 42 | 43 | padx_button = 4 44 | 45 | self.next_i_button = ButtonBox(self.inner, "Next (i)", lambda: self.find_next()) 46 | 47 | self.next_i_button.set_bind( 48 | "", lambda _: self.find_next(reverse=True) 49 | ) 50 | 51 | ToolTip(self.next_i_button, tips["find_next_i"]) 52 | self.next_i_button.grid(row=0, column=1, sticky="ew", padx=padx_button) 53 | 54 | self.next_button = ButtonBox(self.inner, "Next", lambda: self.find_next(False)) 55 | 56 | self.next_button.set_bind( 57 | "", lambda _: self.find_next(False, reverse=True) 58 | ) 59 | 60 | ToolTip(self.next_button, tips["find_next"]) 61 | self.next_button.grid(row=0, column=2, sticky="ew", padx=padx_button) 62 | 63 | self.bound_i_button = ButtonBox( 64 | self.inner, "Bound (i)", lambda: self.find_next(bound=True) 65 | ) 66 | 67 | self.bound_i_button.set_bind( 68 | "", lambda _: self.find_next(bound=True, reverse=True) 69 | ) 70 | 71 | ToolTip(self.bound_i_button, tips["find_bound_i"]) 72 | self.bound_i_button.grid(row=0, column=3, sticky="ew", padx=padx_button) 73 | 74 | self.bound_button = ButtonBox( 75 | self.inner, "Bound", lambda: self.find_next(False, bound=True) 76 | ) 77 | 78 | self.bound_button.set_bind( 79 | "", lambda _: self.find_next(False, bound=True, reverse=True) 80 | ) 81 | 82 | ToolTip(self.bound_button, tips["find_bound"]) 83 | self.bound_button.grid(row=0, column=4, sticky="ew", padx=padx_button) 84 | 85 | self.hide_button = ButtonBox(self.inner, "Hide", lambda: self.hide()) 86 | ToolTip(self.hide_button, tips["find_hide"]) 87 | self.hide_button.grid(row=0, column=5, sticky="ew", padx=padx_button) 88 | 89 | self.inner.grid(row=0, column=0, sticky="ew", padx=4, pady=4) 90 | self.root.grid(row=0, column=0, sticky="ew") 91 | self.root.grid_remove() 92 | 93 | def get_output(self) -> Output | None: 94 | from .display import display 95 | 96 | return display.get_output(self.tab_id) 97 | 98 | def find_next( 99 | self, 100 | case_insensitive: bool = True, 101 | bound: bool = False, 102 | no_match: bool = False, 103 | switch: bool = False, 104 | first_widget: tk.Widget | None = None, 105 | reverse: bool = False, 106 | ) -> None: 107 | if not self.visible: 108 | return 109 | 110 | if not self.widget: 111 | return 112 | 113 | self.clear() 114 | self.entry.focus_set() 115 | query = self.entry.get().strip() 116 | self.entry.set_text(query) 117 | 118 | if not query: 119 | return 120 | 121 | widget = self.widget 122 | 123 | if not widget: 124 | return 125 | 126 | if not first_widget: 127 | first_widget = self.widget 128 | 129 | if reverse and (self.current_match_reverse is not None): 130 | start_pos = widget.index(f"{self.current_match_reverse}+1c") 131 | elif self.current_match is not None: 132 | start_pos = widget.index(f"{self.current_match}+1c") 133 | elif reverse: 134 | start_pos = "end" 135 | else: 136 | start_pos = "1.0" 137 | 138 | if reverse: 139 | end_pos = "1.0" 140 | else: 141 | end_pos = "end" 142 | 143 | if not start_pos: 144 | return 145 | 146 | full_query = query 147 | 148 | if full_query.startswith("/") and full_query.endswith("/"): 149 | full_query = full_query[1:-1] 150 | else: 151 | full_query = re.escape(full_query) 152 | 153 | if bound: 154 | full_query = r"\y" + full_query + r"\y" 155 | 156 | if case_insensitive: 157 | nocase = True 158 | else: 159 | nocase = False 160 | 161 | match_ = self.widget.search( 162 | full_query, 163 | start_pos, 164 | end_pos, 165 | regexp=True, 166 | nocase=nocase, 167 | backwards=reverse, 168 | ) 169 | 170 | output = self.get_output() 171 | 172 | if not output: 173 | return 174 | 175 | if match_: 176 | start_index = widget.index(match_) 177 | end_index = widget.index(f"{start_index}+{len(query)}c") 178 | widget.tag_add("find", start_index, end_index) 179 | widget.tag_configure("find", background=app.theme.find_match_background) 180 | widget.tag_configure("find", foreground=app.theme.find_match_foreground) 181 | end_bbox = widget.bbox(end_index) 182 | 183 | if end_bbox is None: 184 | widget.see(end_index) 185 | else: 186 | widget.see(start_index) 187 | 188 | if (self.widget != output) and (not self.snippet_focused): 189 | if self.snippet == -1: 190 | pos, index = output.get_snippet_index_2(self.widget) 191 | self.snippet_index = pos 192 | self.snippet = index 193 | 194 | output.see(self.snippet_index) 195 | self.snippet_focused = True 196 | 197 | self.current_match_reverse = widget.index(f"{start_index}-1c") 198 | self.current_match = end_index 199 | else: 200 | self.current_match = None 201 | self.current_match_reverse = None 202 | 203 | if no_match: 204 | return 205 | 206 | if switch and (self.widget == first_widget): 207 | return 208 | 209 | self.change_widget() 210 | 211 | self.find_next( 212 | case_insensitive, 213 | bound=bound, 214 | no_match=False, 215 | switch=True, 216 | first_widget=first_widget, 217 | reverse=reverse, 218 | ) 219 | 220 | def next_snippet(self) -> bool: 221 | self.snippet += 1 222 | output = self.get_output() 223 | 224 | if not output: 225 | return False 226 | 227 | if self.snippet >= len(output.snippets): 228 | return False 229 | 230 | snippets = list(reversed(output.snippets)) 231 | self.widget = snippets[self.snippet].text 232 | self.snippet_index = output.get_snippet_index(self.snippet) 233 | self.snippet_focused = False 234 | return True 235 | 236 | def change_widget(self) -> None: 237 | if not self.next_snippet(): 238 | self.widget = self.get_output() 239 | 240 | if not self.widget: 241 | return 242 | 243 | self.snippet = -1 244 | self.snippet_focused = False 245 | self.snippet_index = "1.0" 246 | 247 | def clear(self) -> None: 248 | if self.widget: 249 | self.widget.tag_remove("find", "1.0", "end") 250 | 251 | def show( 252 | self, 253 | widget: tk.Text | None = None, 254 | query: str | None = None, 255 | ) -> None: 256 | if self.widget: 257 | self.clear() 258 | 259 | self.root.grid() 260 | self.entry.set_text("") 261 | self.entry.focus_set() 262 | self.reset() 263 | 264 | if widget: 265 | self.widget = widget 266 | else: 267 | self.widget = self.get_output() 268 | 269 | if not self.widget: 270 | return 271 | 272 | self.visible = True 273 | 274 | if query: 275 | self.entry.set_text(query) 276 | self.find_next() 277 | 278 | def hide(self) -> None: 279 | from .inputcontrol import inputcontrol 280 | 281 | self.clear() 282 | self.root.grid_remove() 283 | inputcontrol.focus() 284 | self.visible = False 285 | self.widget = None 286 | self.snippet = -1 287 | self.snippet_focused = False 288 | self.snippet_index = "1.0" 289 | 290 | def toggle(self) -> None: 291 | if self.visible: 292 | self.hide() 293 | else: 294 | self.show() 295 | 296 | def reset(self) -> None: 297 | self.current_match = None 298 | self.current_match_reverse = None 299 | self.snippet = -1 300 | self.snippet_focused = False 301 | self.snippet_index = "1.0" 302 | -------------------------------------------------------------------------------- /meltdown/findmanager.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | # Standard 4 | import re 5 | import tkinter as tk 6 | 7 | # Modules 8 | from .app import app 9 | from .dialogs import Dialog 10 | 11 | 12 | class FindManager: 13 | def toggle(self) -> None: 14 | from .display import display 15 | 16 | tab = display.get_current_tab() 17 | 18 | if not tab: 19 | return 20 | 21 | tab.get_find().toggle() 22 | 23 | def find( 24 | self, 25 | tab_id: str | None = None, 26 | widget: tk.Text | None = None, 27 | query: str | None = None, 28 | ) -> None: 29 | from .display import display 30 | 31 | if not tab_id: 32 | tab_id = display.current_tab 33 | 34 | tab = display.get_tab(tab_id) 35 | 36 | if not tab: 37 | return 38 | 39 | tab.get_find().show(widget=widget, query=query) 40 | 41 | def find_next(self, case_insensitive: bool = True, reverse: bool = False) -> None: 42 | from .display import display 43 | 44 | tab = display.get_current_tab() 45 | 46 | if not tab: 47 | return 48 | 49 | if not tab.get_find().visible: 50 | tab.get_find().show() 51 | return 52 | 53 | tab.get_find().find_next(case_insensitive, reverse=reverse) 54 | 55 | def find_prev(self, case_insensitive: bool = True) -> None: 56 | from .display import display 57 | 58 | tab = display.get_current_tab() 59 | 60 | if not tab: 61 | return 62 | 63 | if not tab.get_find().visible: 64 | tab.get_find().show() 65 | return 66 | 67 | tab.get_find().find_next(case_insensitive, reverse=True) 68 | 69 | def hide_find(self) -> None: 70 | from .display import display 71 | 72 | tab = display.get_current_tab() 73 | 74 | if not tab: 75 | return 76 | 77 | tab.get_find().hide() 78 | 79 | def find_all(self, query: str | None = None, reverse: bool = False) -> None: 80 | if query: 81 | self.find_all_text(query, reverse=reverse) 82 | else: 83 | Dialog.show_input("Find text in all tabs", lambda s: self.find_all_text(s)) 84 | 85 | def find_all_text(self, query: str, reverse: bool = False) -> None: 86 | from .session import session 87 | from .display import Tab, display 88 | 89 | if not query: 90 | return 91 | 92 | query_lower = query.lower() 93 | is_regex = query.startswith("/") and query.endswith("/") 94 | 95 | if is_regex: 96 | regex_query = query[1:-1] 97 | else: 98 | regex_query = "" 99 | 100 | def find(value: str) -> bool: 101 | if is_regex and re.search(regex_query, value, re.IGNORECASE): 102 | return True 103 | 104 | return query_lower in value.lower() 105 | 106 | def check_tab(tab: Tab) -> bool: 107 | conversation = session.get_conversation(tab.conversation_id) 108 | 109 | if not conversation: 110 | return False 111 | 112 | if conversation.id == "ignore": 113 | return False 114 | 115 | for item in conversation.items: 116 | for key in ["user", "ai", "file"]: 117 | value = getattr(item, key) 118 | 119 | if find(value): 120 | if not tab.loaded: 121 | display.load_tab(tab.tab_id) 122 | app.update() 123 | 124 | display.select_tab(tab.tab_id) 125 | tab.get_find().show(query=query) 126 | return True 127 | 128 | return False 129 | 130 | tabs = [] 131 | index = -1 132 | ids = display.book.ids() 133 | 134 | if reverse: 135 | ids = list(reversed(ids)) 136 | 137 | for id_ in ids: 138 | tab = display.get_tab(id_) 139 | 140 | if tab: 141 | tabs.append(tab) 142 | 143 | if tab.tab_id == display.current_tab: 144 | index = len(tabs) - 1 145 | 146 | if index >= 0: 147 | tabs = tabs[index + 1 :] + tabs[: index + 1] 148 | 149 | for tab in tabs: 150 | if check_tab(tab): 151 | return 152 | 153 | 154 | findmanager = FindManager() 155 | -------------------------------------------------------------------------------- /meltdown/framedata.py: -------------------------------------------------------------------------------- 1 | # Standard 2 | import tkinter as tk 3 | 4 | 5 | class FrameData: 6 | frame_number = 0 7 | 8 | def __init__(self, frame: tk.Frame) -> None: 9 | FrameData.frame_number += 1 10 | self.frame = frame 11 | self.col = 0 12 | 13 | def expand(self) -> None: 14 | self.frame.grid_columnconfigure(self.col - 1, weight=1) 15 | 16 | def do_expand(self, col: int) -> None: 17 | self.frame.grid_columnconfigure(col, weight=1) 18 | 19 | def do_unexpand(self, col: int) -> None: 20 | self.frame.grid_columnconfigure(col, weight=0) 21 | -------------------------------------------------------------------------------- /meltdown/gestures.py: -------------------------------------------------------------------------------- 1 | # Standard 2 | import tkinter as tk 3 | from typing import Any 4 | from collections.abc import Callable 5 | 6 | # Modules 7 | from .args import args 8 | from .commands import commands 9 | from .menus import Menu 10 | 11 | 12 | class Gestures: 13 | def __init__( 14 | self, widget: tk.Widget, text: tk.Text, on_right_click: Callable[..., Any] 15 | ) -> None: 16 | self.on_right_click = on_right_click 17 | self.text = text 18 | 19 | def bind_events(wid: tk.Widget) -> None: 20 | self.bind(wid) 21 | 22 | for child in wid.winfo_children(): 23 | bind_events(child) 24 | 25 | bind_events(widget) 26 | 27 | def bind(self, widget: tk.Widget) -> None: 28 | widget.bind("", lambda e: self.start_drag(e)) 29 | widget.bind("", lambda e: self.on_drag(e)) 30 | widget.bind("", lambda e: self.on_drag_end(e)) 31 | 32 | def reset_drag(self) -> None: 33 | self.drag_x_start = 0 34 | self.drag_y_start = 0 35 | self.drag_x = 0 36 | self.drag_y = 0 37 | 38 | def start_drag(self, event: Any) -> None: 39 | self.drag_x_start = event.x 40 | self.drag_y_start = event.y 41 | self.drag_x = event.x 42 | self.drag_y = event.y 43 | 44 | def on_drag(self, event: Any) -> str: 45 | self.drag_x = event.x 46 | self.drag_y = event.y 47 | return "break" 48 | 49 | def on_drag_end(self, event: Any) -> None: 50 | if args.gestures: 51 | x_diff = abs(self.drag_x - self.drag_x_start) 52 | y_diff = abs(self.drag_y - self.drag_y_start) 53 | 54 | if x_diff > y_diff: 55 | if x_diff >= args.gestures_threshold: 56 | if (self.drag_x < self.drag_x_start) and args.gestures_left: 57 | Menu.hide_all() 58 | commands.exec(args.gestures_left) 59 | elif args.gestures_right: 60 | commands.exec(args.gestures_right) 61 | 62 | return 63 | elif y_diff >= args.gestures_threshold: 64 | if (self.drag_y < self.drag_y_start) and args.gestures_up: 65 | Menu.hide_all() 66 | commands.exec(args.gestures_up) 67 | elif args.gestures_down: 68 | Menu.hide_all() 69 | commands.exec(args.gestures_down) 70 | 71 | return 72 | 73 | if event.widget == self.text: 74 | self.on_right_click(event) 75 | -------------------------------------------------------------------------------- /meltdown/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Merkoba/Meltdown/4265a033c2a5a5845ee03810871cf712e7909a1f/meltdown/icon.png -------------------------------------------------------------------------------- /meltdown/image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Merkoba/Meltdown/4265a033c2a5a5845ee03810871cf712e7909a1f/meltdown/image.jpg -------------------------------------------------------------------------------- /meltdown/itemops.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | # Standard 4 | from typing import TYPE_CHECKING 5 | 6 | # Modules 7 | from .app import app 8 | from .args import args 9 | from .utils import utils 10 | from .dialogs import Dialog, Commands 11 | from .memory import memory 12 | 13 | 14 | if TYPE_CHECKING: 15 | from .session import Item 16 | 17 | 18 | class ItemOps: 19 | def action( 20 | self, 21 | mode: str, 22 | number: str, 23 | tab_id: str | None = None, 24 | no_history: bool = False, 25 | who: str = "both", 26 | ) -> None: 27 | from .model import model 28 | from .display import display 29 | from .output import Output 30 | 31 | tabconvo = display.get_tab_convo(tab_id) 32 | 33 | if not tabconvo: 34 | return 35 | 36 | if not tabconvo.convo.items: 37 | return 38 | 39 | index = utils.get_index(number, tabconvo.convo.items) 40 | 41 | if index < 0: 42 | return 43 | 44 | if index >= len(tabconvo.convo.items): 45 | return 46 | 47 | item = tabconvo.convo.items[index] 48 | 49 | if not item: 50 | return 51 | 52 | user_text = item.user 53 | ai_text = item.ai 54 | file = item.file 55 | 56 | both_text = f"User: {user_text}\n\nAI: {ai_text}" 57 | 58 | if mode == "repeat": 59 | if user_text: 60 | prompt = {"text": user_text, "file": file, "no_history": no_history} 61 | model.stream(prompt, tabconvo.tab.tab_id) 62 | elif mode == "copy": 63 | text = "" 64 | 65 | if who: 66 | if who == "user": 67 | text = user_text 68 | elif who == "ai": 69 | text = ai_text 70 | elif who == "both": 71 | texts = [] 72 | 73 | if user_text: 74 | text = "" 75 | text += display.get_prompt("user", put_colons=False) 76 | text += f": {user_text}" 77 | 78 | if file: 79 | text += f"\n\nFile: {file}" 80 | 81 | texts.append(text) 82 | 83 | if ai_text: 84 | text = "" 85 | text += display.get_prompt("ai", put_colons=False) 86 | text += f": {ai_text}" 87 | texts.append(text) 88 | 89 | text = "\n\n".join(texts) 90 | 91 | if not text: 92 | return 93 | 94 | utils.copy(text, command=True) 95 | elif mode == "select": 96 | Output.select_item(index + 1) 97 | elif mode == "use": 98 | if who == "user": 99 | if user_text: 100 | self.do_use_item(user_text) 101 | elif who == "ai": 102 | if ai_text: 103 | self.do_use_item(ai_text) 104 | elif who == "both": 105 | if user_text and ai_text: 106 | self.do_use_item(both_text) 107 | elif mode == "info": 108 | self.do_show_info(item) 109 | 110 | def repeat(self, number: str, no_history: bool = False) -> None: 111 | if not number: 112 | number = "last" 113 | 114 | self.action("repeat", number, no_history=no_history) 115 | 116 | def copy(self, number: str) -> None: 117 | if not number: 118 | number = "last" 119 | 120 | self.action("copy", number) 121 | 122 | def select(self, number: str) -> None: 123 | if not number: 124 | number = "last" 125 | 126 | self.action("select", number) 127 | 128 | def use_item(self, number: str) -> None: 129 | if not number: 130 | number = "last" 131 | 132 | who = "ai" 133 | 134 | if args.use_both: 135 | who = "both" 136 | 137 | self.action("use", number, who=who) 138 | 139 | def do_use_item(self, text: str) -> None: 140 | from .formats import formats 141 | 142 | def action(mode: str) -> None: 143 | formats.do_open(mode, text=text) 144 | 145 | cmds = Commands() 146 | cmds.add("Program", lambda a: self.run_program(text)) 147 | cmds.add("Markdown", lambda a: action("markdown")) 148 | cmds.add("Text", lambda a: action("text")) 149 | 150 | Dialog.show_dialog("Use Item", cmds) 151 | 152 | def run_program( 153 | self, text: str | None = None, program: str | None = None, auto: bool = False 154 | ) -> None: 155 | if not text: 156 | last_item = self.last_item() 157 | 158 | if last_item: 159 | text = last_item.ai 160 | 161 | if not text: 162 | return 163 | 164 | def run() -> None: 165 | if program: 166 | memory.set_value("last_program", program) 167 | app.run_program(program, text) 168 | 169 | if program: 170 | run() 171 | return 172 | 173 | if auto and memory.last_program: 174 | program = memory.last_program 175 | run() 176 | return 177 | 178 | def action(ans: str) -> None: 179 | nonlocal program 180 | 181 | if not ans: 182 | return 183 | 184 | program = ans 185 | run() 186 | 187 | if args.use_program: 188 | action(args.use_program) 189 | return 190 | 191 | Dialog.show_input("Run Program", lambda a: action(a), value=memory.last_program) 192 | 193 | def show_info(self, number: str) -> None: 194 | if not number: 195 | number = "last" 196 | 197 | who = "ai" 198 | 199 | if args.use_both: 200 | who = "both" 201 | 202 | self.action("info", number, who=who) 203 | 204 | def do_show_info(self, item: Item) -> None: 205 | text = "" 206 | 207 | if item.model: 208 | text += utils.no_break(f"Model: {item.model}") 209 | 210 | if item.date is not None: 211 | text += f"\n\n{utils.to_date(item.date)}\n" 212 | text += utils.time_ago(item.date, utils.now()) 213 | 214 | if item.duration is not None: 215 | text += f"\n\nDuration: {item.duration:.2f} seconds" 216 | 217 | if item.user: 218 | w_user = len(utils.get_words(item.user)) 219 | words = utils.singular_or_plural(w_user, "word", "words") 220 | text += f"\n\nUser: {w_user} {words} ({len(item.user)} chars)" 221 | 222 | if item.ai: 223 | w_ai = len(utils.get_words(item.ai)) 224 | words = utils.singular_or_plural(w_ai, "word", "words") 225 | text += f"\nAI: {w_ai} {words} ({len(item.ai)} chars)" 226 | 227 | if item.file: 228 | text += utils.no_break(f"\n\nFile: {item.file}") 229 | 230 | if item.seed is not None: 231 | text += f"\n\nSeed: {item.seed}" 232 | 233 | if item.history is not None: 234 | text += f"\nHistory: {item.history}" 235 | 236 | if item.max_tokens is not None: 237 | text += f"\nMax Tokens: {item.max_tokens}" 238 | 239 | if item.temperature is not None: 240 | text += f"\nTemperature: {item.temperature}" 241 | 242 | if item.top_k is not None: 243 | text += f"\nTop K: {item.top_k}" 244 | 245 | if item.top_p is not None: 246 | text += f"\nTop P: {item.top_p}" 247 | 248 | Dialog.show_msgbox("Information", text) 249 | 250 | def last_item(self) -> Item | None: 251 | from .display import display 252 | 253 | tabconvo = display.get_tab_convo() 254 | 255 | if not tabconvo: 256 | return None 257 | 258 | if not tabconvo.convo.items: 259 | return None 260 | 261 | return tabconvo.convo.items[-1] 262 | 263 | 264 | itemops = ItemOps() 265 | -------------------------------------------------------------------------------- /meltdown/light_theme.py: -------------------------------------------------------------------------------- 1 | # Modules 2 | from .theme import Theme 3 | 4 | 5 | class LightTheme(Theme): 6 | def __init__(self) -> None: 7 | super().__init__() 8 | self.background_color = "white" 9 | self.foreground_color = "#343434" 10 | 11 | self.output_background = "white" 12 | self.output_foreground = "#343434" 13 | 14 | self.entry_background = "#C5C5C5" 15 | self.entry_foreground = "#343434" 16 | self.entry_placeholder_color = "grey" 17 | self.entry_insert = "#343434" 18 | 19 | self.user_color = "#343434" 20 | self.ai_color = "#343434" 21 | 22 | self.tab_normal_background = "#AEAEAE" 23 | self.tab_normal_foreground = "#343434" 24 | self.tab_selected_background = "#969696" 25 | self.tab_selected_foreground = "white" 26 | self.tabs_container_color = "#BABABA" 27 | self.tab_border = "#37373D" 28 | self.tab_picked_border = "#21ffff" 29 | 30 | self.button_background = "#6A7B83" 31 | self.button_foreground = "white" 32 | self.button_hover_background = "#86A9AD" 33 | 34 | self.button_alt_background = "#B1BBBC" 35 | self.button_alt_hover_background = "#A8BCBE" 36 | 37 | self.button_highlight_background = "#3F9687" 38 | self.button_highlight_hover_background = "#5FA086" 39 | 40 | self.button_active_background = "#3F9687" 41 | self.button_active_hover_background = "#944E63" 42 | 43 | self.button_disabled_background = "#C5C5C5" 44 | 45 | self.combobox_background = "#C5C5C5" 46 | self.combobox_foreground = "#343434" 47 | 48 | self.snippet_background = "#CBCBCB" 49 | self.snippet_foreground = "#343434" 50 | self.snippet_header_background = "#7B7B7B" 51 | self.snippet_header_foreground = "white" 52 | self.syntax_style = "sas" 53 | 54 | self.scrollbar_1 = "#BABABA" 55 | self.scrollbar_2 = "#949494" 56 | 57 | self.menu_hover_background = "#6A7B83" 58 | self.menu_canvas_background = "#949494" 59 | 60 | self.button_highlight_background = "#44B3A1" 61 | 62 | self.output_selection_background = "#677577" 63 | self.output_selection_foreground = "white" 64 | self.snippet_selection_background = "#677577" 65 | self.snippet_selection_foreground = "white" 66 | self.entry_selection_background = "#677577" 67 | self.entry_selection_foreground = "white" 68 | 69 | self.tooltip_background = "#2B303B" 70 | self.tooltip_foreground = "white" 71 | 72 | self.find_background = "#8799C2" 73 | self.find_match_background = "#677577" 74 | self.find_match_foreground = "white" 75 | 76 | self.effect_color = "#0a74ff" 77 | -------------------------------------------------------------------------------- /meltdown/listener.py: -------------------------------------------------------------------------------- 1 | # Standard 2 | import time 3 | import threading 4 | import tempfile 5 | from typing import Any 6 | from pathlib import Path 7 | 8 | # Libraries 9 | from watchdog.observers import Observer # type: ignore 10 | from watchdog.events import FileSystemEventHandler # type: ignore 11 | 12 | # Modules 13 | from .args import args 14 | from .app import app 15 | from .utils import utils 16 | from .files import files 17 | from .inputcontrol import inputcontrol 18 | 19 | 20 | class FileChangeHandler(FileSystemEventHandler): # type: ignore 21 | def __init__(self, path: Path) -> None: 22 | self.path = path 23 | 24 | def on_modified(self, event: Any) -> None: 25 | if event.src_path == str(self.path): 26 | try: 27 | text = files.read(self.path).strip() 28 | 29 | if text: 30 | files.write(self.path, "") 31 | inputcontrol.submit(text=text) 32 | except Exception as e: 33 | utils.msg(f"Listener error: {e!s}") 34 | 35 | 36 | class Listener: 37 | def start(self) -> None: 38 | if not args.listen: 39 | return 40 | 41 | thread = threading.Thread(target=lambda: self.do_start()) 42 | thread.daemon = True 43 | thread.start() 44 | 45 | def do_start(self) -> None: 46 | program = app.manifest["program"] 47 | 48 | if args.listen_file: 49 | path = Path(args.listen_file) 50 | else: 51 | file_name = f"mlt_{program}.input" 52 | path = Path(tempfile.gettempdir(), file_name) 53 | 54 | if not args.quiet: 55 | utils.msg(f"Listening: {path!s}") 56 | 57 | handler = FileChangeHandler(path) 58 | observer = Observer() 59 | observer.schedule(handler, path.parent, recursive=False) 60 | 61 | thread = threading.Thread(target=observer.start) 62 | thread.daemon = True 63 | thread.start() 64 | 65 | try: 66 | while True: 67 | time.sleep(1) 68 | finally: 69 | observer.stop() 70 | observer.join() 71 | 72 | 73 | listener = Listener() 74 | -------------------------------------------------------------------------------- /meltdown/logs.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | # Standard 4 | import json 5 | from pathlib import Path 6 | 7 | # Modules 8 | from .app import app 9 | from .config import config 10 | from .dialogs import Dialog, Commands 11 | from .display import display 12 | from .args import args 13 | from .session import session 14 | from .session import Conversation 15 | from .paths import paths 16 | from .utils import utils 17 | from .files import files 18 | from .formats import formats 19 | from .memory import memory 20 | 21 | 22 | class Logs: 23 | def menu(self) -> None: 24 | cmds = Commands() 25 | cmds.add("Open", lambda a: self.open_directory()) 26 | cmds.add("Last", lambda a: self.open_last_log()) 27 | cmds.add("Save", lambda a: self.save_menu()) 28 | 29 | Dialog.show_dialog("Logs Menu", commands=cmds) 30 | 31 | def save_menu(self, full: bool = True, tab_id: str | None = None) -> None: 32 | cmds = Commands() 33 | 34 | if full: 35 | cmds.add("Save All", lambda a: self.save_all()) 36 | 37 | cmds.add("Markdown", lambda a: self.to_markdown(tab_id=tab_id)) 38 | cmds.add("JSON", lambda a: self.to_json(tab_id=tab_id)) 39 | cmds.add("Text", lambda a: self.to_text(tab_id=tab_id)) 40 | 41 | Dialog.show_dialog("Save conversation to a file?", cmds) 42 | 43 | def save_all(self) -> None: 44 | cmds = Commands() 45 | cmds.add("Markdown", lambda a: self.to_markdown(True)) 46 | cmds.add("JSON", lambda a: self.to_json(True)) 47 | cmds.add("Text", lambda a: self.to_text(True)) 48 | 49 | Dialog.show_dialog("Save all conversations?", cmds) 50 | 51 | def save_file( 52 | self, text: str, name: str, ext: str, save_all: bool, overwrite: bool, mode: str 53 | ) -> str: 54 | text = text.strip() 55 | paths.logs.mkdir(parents=True, exist_ok=True) 56 | file_name = name + f".{ext}" 57 | file_path = Path(paths.logs, file_name) 58 | num = 2 59 | 60 | if (not overwrite) and args.increment_logs: 61 | while file_path.exists(): 62 | file_name = f"{name}_{num}.{ext}" 63 | file_path = Path(paths.logs, file_name) 64 | num += 1 65 | 66 | if num > 9999: 67 | break 68 | 69 | files.write(file_path, text) 70 | 71 | if not save_all: 72 | if not args.quiet and args.log_feedback: 73 | utils.saved_path(file_path) 74 | 75 | cmd = "" 76 | 77 | if args.open_on_log: 78 | app.open_generic(str(file_path)) 79 | else: 80 | if (mode == "text") and args.on_log_text: 81 | cmd = args.on_log_text 82 | elif (mode == "json") and args.on_log_json: 83 | cmd = args.on_log_json 84 | elif (mode == "markdown") and args.on_log_markdown: 85 | cmd = args.on_log_markdown 86 | elif args.on_log: 87 | cmd = args.on_log 88 | 89 | if cmd: 90 | app.run_program(cmd, str(file_path)) 91 | 92 | return str(file_path) 93 | 94 | def save( 95 | self, 96 | mode: str, 97 | save_all: bool, 98 | name: str | None = None, 99 | tab_id: str | None = None, 100 | ) -> None: 101 | num = 0 102 | last_log = "" 103 | ext = formats.get_ext(mode) 104 | overwrite = bool(name) 105 | 106 | def save(content: str, name_: str) -> None: 107 | nonlocal num, last_log 108 | 109 | if not content: 110 | return 111 | 112 | num += 1 113 | 114 | if args.clean_names: 115 | name_ = utils.clean_name(name_) 116 | 117 | name_ = name_[: config.max_file_name_length].strip(" _") 118 | 119 | last_log = self.save_file( 120 | content, name_, ext, save_all, overwrite=overwrite, mode=mode 121 | ) 122 | 123 | if save_all: 124 | conversations = [ 125 | session.get_conversation(key) for key in session.conversations 126 | ] 127 | else: 128 | tabconvo = display.get_tab_convo(tab_id) 129 | 130 | if not tabconvo: 131 | return 132 | 133 | conversations = [tabconvo.convo] 134 | 135 | if (len(conversations) > 1) and args.concat_logs: 136 | contents = [] 137 | 138 | for conversation in conversations: 139 | if not conversation: 140 | continue 141 | 142 | if not conversation.items: 143 | continue 144 | 145 | contents.append(self.get_content(mode, conversation)) 146 | 147 | if mode == "text": 148 | content = "\n\n---\n\n".join(contents) 149 | elif mode == "json": 150 | cont = "[\n" + ",\n".join(contents) + "\n]" 151 | content = json.dumps(json.loads(cont), indent=4) 152 | elif mode == "markdown": 153 | content = "\n\n---\n\n".join(contents) 154 | else: 155 | content = "" 156 | 157 | name_ = f"{len(contents)}_{utils.random_word()}" 158 | save(content, name_) 159 | else: 160 | for conversation in conversations: 161 | if not conversation: 162 | continue 163 | 164 | if not conversation.items: 165 | continue 166 | 167 | content = self.get_content(mode, conversation) 168 | name_ = name or conversation.name 169 | save(content, name_) 170 | 171 | if save_all: 172 | if args.quiet or (not args.log_feedback): 173 | return 174 | 175 | f_type = formats.get_name(mode) 176 | word = utils.singular_or_plural(num, "log", "logs") 177 | msg = f"{num} {f_type} {word} saved." 178 | display.print(utils.emoji_text(msg, "storage")) 179 | 180 | if last_log: 181 | memory.set_value("last_log", last_log) 182 | 183 | def to_json( 184 | self, 185 | save_all: bool = False, 186 | name: str | None = None, 187 | tab_id: str | None = None, 188 | ) -> None: 189 | self.save("json", save_all, name, tab_id=tab_id) 190 | 191 | def to_markdown( 192 | self, 193 | save_all: bool = False, 194 | name: str | None = None, 195 | tab_id: str | None = None, 196 | ) -> None: 197 | self.save("markdown", save_all, name, tab_id=tab_id) 198 | 199 | def get_json(self, conversation: Conversation) -> str: 200 | if not conversation: 201 | return "" 202 | 203 | if not conversation.items: 204 | return "" 205 | 206 | return formats.get_json(conversation, name_mode="log") 207 | 208 | def to_text( 209 | self, 210 | save_all: bool = False, 211 | name: str | None = None, 212 | tab_id: str | None = None, 213 | ) -> None: 214 | self.save("text", save_all, name, tab_id=tab_id) 215 | 216 | def get_text(self, conversation: Conversation) -> str: 217 | if not conversation: 218 | return "" 219 | 220 | if not conversation.items: 221 | return "" 222 | 223 | text = formats.get_text(conversation, name_mode="log") 224 | 225 | if not text: 226 | return "" 227 | 228 | full_text = "" 229 | full_text += f"Name: {conversation.name}\n" 230 | 231 | date_created = utils.to_date(conversation.created) 232 | full_text += f"Created: {date_created}\n" 233 | 234 | date_saved = utils.to_date(utils.now()) 235 | full_text += f"Saved: {date_saved}" 236 | 237 | full_text += "\n\n---\n\n" 238 | full_text += text 239 | 240 | return full_text 241 | 242 | def get_markdown(self, conversation: Conversation) -> str: 243 | if not conversation: 244 | return "" 245 | 246 | if not conversation.items: 247 | return "" 248 | 249 | text = formats.get_markdown(conversation, name_mode="log") 250 | 251 | if not text: 252 | return "" 253 | 254 | full_text = "" 255 | full_text += f"# {conversation.name}\n\n" 256 | 257 | date_created = utils.to_date(conversation.created) 258 | full_text += f"**Created:** {date_created}\n" 259 | 260 | date_saved = utils.to_date(utils.now()) 261 | full_text += f"**Saved:** {date_saved}" 262 | 263 | full_text += "\n\n---\n\n" 264 | full_text += text 265 | 266 | return full_text 267 | 268 | def open_last_log(self) -> None: 269 | if not memory.last_log: 270 | return 271 | 272 | app.open_generic(memory.last_log) 273 | 274 | def get_content(self, mode: str, conversation: Conversation) -> str: 275 | if mode == "text": 276 | text = self.get_text(conversation) 277 | elif mode == "json": 278 | text = self.get_json(conversation) 279 | elif mode == "markdown": 280 | text = self.get_markdown(conversation) 281 | else: 282 | text = "" 283 | 284 | return text.strip() 285 | 286 | def open_directory(self) -> None: 287 | paths.logs.mkdir(parents=True, exist_ok=True) 288 | app.open_generic(str(paths.logs)) 289 | 290 | 291 | logs = Logs() 292 | -------------------------------------------------------------------------------- /meltdown/main.py: -------------------------------------------------------------------------------- 1 | # Standard 2 | import os 3 | import fcntl 4 | import tempfile 5 | from pathlib import Path 6 | 7 | # Modules 8 | from .app import app 9 | from .config import config 10 | from .widgets import widgets 11 | from .display import display 12 | from .model import model 13 | from .session import session 14 | from .args import args 15 | from .commands import commands 16 | from .keyboard import keyboard 17 | from .inputcontrol import inputcontrol 18 | from .utils import utils 19 | from .paths import paths 20 | from .system import system 21 | from .console import console 22 | from .listener import listener 23 | from .tasks import tasks 24 | from .memory import memory 25 | from .autoscroll import autoscroll 26 | from .variables import variables 27 | 28 | 29 | def main() -> None: 30 | now = utils.now() 31 | title = app.manifest["title"] 32 | program = app.manifest["program"] 33 | args.parse() 34 | 35 | if not paths.setup(): 36 | return 37 | 38 | if args.profile: 39 | pid = f"mlt_{program}_{args.profile}.pid" 40 | else: 41 | pid = f"mlt_{program}.pid" 42 | 43 | pid_file = Path(tempfile.gettempdir(), pid) 44 | fp = pid_file.open("w", encoding="utf-8") 45 | 46 | try: 47 | fcntl.lockf(fp, fcntl.LOCK_EX | fcntl.LOCK_NB) 48 | except OSError: 49 | if not args.force: 50 | utils.msg( 51 | f"{title} is already running.\nUse --force to launch multiple instances." 52 | ) 53 | 54 | return 55 | 56 | memory.load() 57 | config.load() 58 | app.prepare() 59 | widgets.make() 60 | autoscroll.setup() 61 | model.setup() 62 | display.make() 63 | session.load() 64 | widgets.setup() 65 | keyboard.setup() 66 | commands.setup() 67 | variables.setup() 68 | inputcontrol.setup() 69 | app.setup(now) 70 | system.start() 71 | console.start() 72 | listener.start() 73 | tasks.start_all() 74 | 75 | # Create singleton 76 | fp.write(str(os.getpid())) 77 | fp.flush() 78 | 79 | if args.time and (not args.quiet): 80 | msg, now = utils.check_time("Ready", now) 81 | utils.msg(msg) 82 | 83 | try: 84 | app.run() 85 | except KeyboardInterrupt: 86 | pass 87 | except BaseException as e: 88 | utils.error(e) 89 | 90 | try: 91 | model.unload() 92 | except KeyboardInterrupt: 93 | pass 94 | except BaseException as e: 95 | utils.error(e) 96 | 97 | 98 | if __name__ == "__main__": 99 | main() 100 | -------------------------------------------------------------------------------- /meltdown/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "273.0.0", 3 | "title": "Meltdown", 4 | "program": "meltdown", 5 | "author": "Merkoba", 6 | "repo": "github.com/Merkoba/Meltdown", 7 | "description": "Interface to AI" 8 | } 9 | -------------------------------------------------------------------------------- /meltdown/memory.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | # Standard 4 | from typing import Any 5 | 6 | 7 | class Memory: 8 | def __init__(self) -> None: 9 | self.last_log = "" 10 | self.last_program = "" 11 | self.last_config = "" 12 | self.last_session = "" 13 | 14 | def load(self) -> None: 15 | from .paths import paths 16 | from .files import files 17 | 18 | try: 19 | mem = files.load(paths.memory) 20 | except BaseException: 21 | return 22 | 23 | for key in mem: 24 | if hasattr(self, key): 25 | setattr(self, key, mem[key]) 26 | 27 | def save(self) -> None: 28 | from .paths import paths 29 | from .files import files 30 | 31 | mem = {} 32 | 33 | for key in self.__dict__: 34 | if key.startswith("last_"): 35 | mem[key] = getattr(self, key) 36 | 37 | files.save(paths.memory, mem) 38 | 39 | def set_value(self, key: str, value: Any) -> None: 40 | if getattr(self, key) == value: 41 | return 42 | 43 | setattr(self, key, value) 44 | self.save() 45 | 46 | 47 | memory = Memory() 48 | -------------------------------------------------------------------------------- /meltdown/modelcontrol.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | # Standard 4 | import tkinter as tk 5 | from typing import Any 6 | from tkinter import filedialog 7 | 8 | # Modules 9 | from .app import app 10 | from .config import config 11 | from .model import model 12 | from .entrybox import EntryBox 13 | from .files import files 14 | 15 | 16 | class ModelControl: 17 | def __init__(self) -> None: 18 | self.history_index = -1 19 | self.model: EntryBox 20 | 21 | def fill(self) -> None: 22 | from .widgets import widgets 23 | 24 | self.model = widgets.model 25 | 26 | def bind(self) -> None: 27 | self.model.bind("", lambda e: self.show_context(e)) 28 | 29 | def show_context( 30 | self, 31 | event: Any = None, 32 | only_items: bool = False, 33 | target: tk.Widget | None = None, 34 | ) -> None: 35 | from .widgets import widgets 36 | 37 | if model.model_loading: 38 | return 39 | 40 | widgets.show_menu_items( 41 | "model", 42 | "models", 43 | lambda m: self.set(m), 44 | event, 45 | only_items=only_items, 46 | alt_cmd=lambda m: self.forget(m, event), 47 | target=target, 48 | ) 49 | 50 | def set(self, m: str) -> None: 51 | config.set("model", m) 52 | 53 | def apply_history(self, inputs: list[str]) -> None: 54 | text = inputs[self.history_index] 55 | self.set(text) 56 | self.model.focus_end() 57 | 58 | def get_history_list(self) -> list[str]: 59 | return files.get_list("models") 60 | 61 | def history_up(self) -> None: 62 | models = self.get_history_list() 63 | 64 | if not models: 65 | return 66 | 67 | if self.history_index == -1: 68 | self.history_index = 0 69 | else: 70 | self.history_index = (self.history_index + 1) % len(models) 71 | 72 | self.apply_history(models) 73 | 74 | def history_down(self) -> None: 75 | models = self.get_history_list() 76 | 77 | if not models: 78 | return 79 | 80 | if self.history_index == -1: 81 | self.history_index = len(models) - 1 82 | else: 83 | self.history_index = (self.history_index - 1) % len(models) 84 | 85 | self.apply_history(models) 86 | 87 | def reset_history_index(self) -> None: 88 | self.history_index = -1 89 | 90 | def forget(self, m: str, event: Any) -> None: 91 | files.remove_model(m) 92 | self.show_context(event) 93 | 94 | def show_recent(self, target: tk.Widget | None = None) -> None: 95 | self.show_context(only_items=True, target=target) 96 | 97 | def browse(self) -> None: 98 | from .widgets import widgets 99 | 100 | if model.model_loading: 101 | return 102 | 103 | file = filedialog.askopenfilename(initialdir=widgets.get_dir("model", "models")) 104 | 105 | if file: 106 | self.set(file) 107 | 108 | def change(self, name: str) -> None: 109 | if not name: 110 | return 111 | 112 | name = name.lower() 113 | list = files.get_list("models") 114 | 115 | if not list: 116 | return 117 | 118 | for item in list: 119 | if name in item.lower(): 120 | self.set(item) 121 | return 122 | 123 | def is_focused(self) -> bool: 124 | focused = app.focused() 125 | 126 | if not focused: 127 | return False 128 | 129 | if isinstance(focused, EntryBox): 130 | return focused == self.model 131 | 132 | return False 133 | 134 | 135 | modelcontrol = ModelControl() 136 | -------------------------------------------------------------------------------- /meltdown/paths.py: -------------------------------------------------------------------------------- 1 | # Standard 2 | from pathlib import Path 3 | 4 | # Libaries 5 | import appdirs # type: ignore 6 | 7 | # Modules 8 | from .app import app 9 | from .args import args 10 | from .utils import utils 11 | 12 | 13 | class Paths: 14 | def __init__(self) -> None: 15 | self.config: Path 16 | self.models: Path 17 | self.inputs: Path 18 | self.systems: Path 19 | self.files: Path 20 | self.session: Path 21 | self.commands: Path 22 | self.autocomplete: Path 23 | self.memory: Path 24 | self.configs: Path 25 | self.sessions: Path 26 | self.logs: Path 27 | self.openai_key: Path 28 | self.google_key: Path 29 | self.errors: Path 30 | self.nouns: Path 31 | 32 | def error(self, what: str) -> None: 33 | utils.msg(f"Error: Can't find or create the '{what}' directory.") 34 | 35 | def setup(self) -> bool: 36 | program = app.manifest["program"] 37 | location = Path(program, args.profile) 38 | 39 | # Config 40 | 41 | try: 42 | if args.config_dir: 43 | config_dir = Path(args.config_dir) 44 | else: 45 | config_dir = Path(appdirs.user_config_dir()) 46 | 47 | config_dir.mkdir(parents=True, exist_ok=True) 48 | self.config_dir = Path(config_dir, location) 49 | self.config = Path(self.config_dir, "config.json") 50 | self.configs = Path(self.config_dir, "configs") 51 | except Exception: 52 | self.error("config") 53 | return False 54 | 55 | # Data 56 | 57 | try: 58 | if args.data_dir: 59 | data_dir = Path(args.data_dir) 60 | else: 61 | data_dir = Path(appdirs.user_data_dir()) 62 | 63 | data_dir.mkdir(parents=True, exist_ok=True) 64 | except Exception: 65 | self.error("data") 66 | return False 67 | 68 | self.data_dir = Path(data_dir, location) 69 | self.sessions = Path(self.data_dir, "sessions") 70 | self.inputs = Path(self.data_dir, "inputs.json") 71 | self.files = Path(self.data_dir, "files.json") 72 | self.session = Path(self.data_dir, "session.json") 73 | self.autocomplete = Path(self.data_dir, "autocomplete.json") 74 | self.commands = Path(self.data_dir, "commands.json") 75 | self.models = Path(self.data_dir, "models.json") 76 | self.systems = Path(self.data_dir, "systems.json") 77 | self.memory = Path(self.data_dir, "memory.json") 78 | 79 | if args.logs_dir: 80 | self.logs = Path(args.logs_dir) 81 | else: 82 | self.logs = Path(self.data_dir, "logs") 83 | 84 | self.openai_key = Path(self.data_dir, "openai_key.txt") 85 | self.google_key = Path(self.data_dir, "google_key.txt") 86 | self.errors = Path(self.data_dir, "errors") 87 | 88 | # Assets 89 | 90 | self.nouns = Path(app.here, "nouns.txt") 91 | return True 92 | 93 | 94 | paths = Paths() 95 | -------------------------------------------------------------------------------- /meltdown/portrait.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Merkoba/Meltdown/4265a033c2a5a5845ee03810871cf712e7909a1f/meltdown/portrait.jpg -------------------------------------------------------------------------------- /meltdown/rentry.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | # Standard 4 | import threading 5 | import urllib.parse 6 | from pathlib import Path 7 | from http import HTTPStatus 8 | from typing import Callable 9 | 10 | # Libraries 11 | import requests # type: ignore 12 | 13 | # Modules 14 | from .config import config 15 | 16 | 17 | Action = Callable[[str, str, str], None] 18 | 19 | 20 | class Rentry: 21 | def __init__( 22 | self, 23 | text: str, 24 | password: str, 25 | tab_id: str, 26 | after_upload: Action, 27 | ) -> None: 28 | self.text = text 29 | self.password = password 30 | self.tab_id = tab_id 31 | self.after_upload = after_upload 32 | self.timeout = 10 33 | 34 | self.headers = { 35 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/117.0", 36 | "Referer": config.rentry_site, 37 | "Sec-Fetch-Dest": "document", 38 | "Sec-Fetch-Mode": "navigate", 39 | "Sec-Fetch-Site": "same-origin", 40 | "Sec-Fetch-User": "?1", 41 | } 42 | 43 | thread = threading.Thread(target=lambda: self.post()) 44 | thread.daemon = True 45 | thread.start() 46 | 47 | def get_cookie(self, cookie_name: str) -> str: 48 | return str(self.session.cookies.get(cookie_name, default="")) 49 | 50 | def get_token(self) -> str: 51 | return self.get_cookie("csrftoken") 52 | 53 | def post(self) -> None: 54 | self.session = requests.session() 55 | self.session.get(config.rentry_site) 56 | 57 | res = self.session.post( 58 | config.rentry_site, 59 | headers=self.headers, 60 | timeout=self.timeout, 61 | data={ 62 | "csrfmiddlewaretoken": self.get_token(), 63 | "text": (self.text if len(self.text) > 0 else "."), 64 | "edit_code": self.password, 65 | }, 66 | allow_redirects=False, 67 | ) 68 | 69 | if res.status_code != HTTPStatus.FOUND: 70 | return 71 | 72 | url = urllib.parse.urlparse(res.headers["Location"]) 73 | url = Path(url.path).name 74 | full_url = f"{config.rentry_site}/{url}" 75 | self.after_upload(full_url, self.password, self.tab_id) 76 | -------------------------------------------------------------------------------- /meltdown/ruff.toml: -------------------------------------------------------------------------------- 1 | [lint] 2 | 3 | preview = true 4 | 5 | select = [ 6 | "T", 7 | "Q", 8 | "W", 9 | "B", 10 | "N", 11 | "F", 12 | "FA", 13 | "RET", 14 | "PTH", 15 | "ERA", 16 | "PLW", 17 | "PERF", 18 | "RUF", 19 | "FLY", 20 | "PT", 21 | "PYI", 22 | "PIE", 23 | "ICN", 24 | "UP", 25 | "TRY", 26 | "C4", 27 | "E401", 28 | "E713", 29 | "E721", 30 | "S101", 31 | "S113", 32 | "SIM103", 33 | "SIM114", 34 | "SIM118", 35 | "SIM210", 36 | "PLR5501", 37 | "PLR1711", 38 | ] 39 | 40 | ignore = [ 41 | "PLW0108", 42 | ] 43 | 44 | exclude = [ 45 | "tests.py", 46 | ] -------------------------------------------------------------------------------- /meltdown/scrollers.py: -------------------------------------------------------------------------------- 1 | # Standard 2 | from typing import TYPE_CHECKING 3 | 4 | # Modules 5 | from .args import args 6 | from .tooltips import ToolTip 7 | from .tips import tips 8 | from .widgetutils import widgetutils 9 | 10 | 11 | if TYPE_CHECKING: 12 | from .widgets import Widgets 13 | 14 | 15 | class Scrollers: 16 | def __init__(self) -> None: 17 | self.left_icon = "<" 18 | self.right_icon = ">" 19 | 20 | def setup(self) -> None: 21 | self.do_setup("system") 22 | self.do_setup("details_1") 23 | self.do_setup("details_2") 24 | self.check_all_buttons() 25 | 26 | def do_setup(self, name: str) -> None: 27 | from .widgets import widgets 28 | 29 | scroller = getattr(widgets, f"scroller_{name}") 30 | scroller.update_idletasks() 31 | canvas = getattr(widgets, f"scroller_canvas_{name}") 32 | canvas.update_idletasks() 33 | canvas.configure(width=scroller.winfo_reqwidth()) 34 | canvas.configure(height=scroller.winfo_reqheight()) 35 | left = getattr(widgets, f"scroller_button_left_{name}") 36 | right = getattr(widgets, f"scroller_button_right_{name}") 37 | 38 | left.set_bind("", lambda e: self.to_left(name)) 39 | left.set_bind("", lambda e: self.to_right(name)) 40 | left.set_bind("", lambda e: self.to_start(name)) 41 | 42 | right.set_bind("", lambda e: self.to_left(name)) 43 | right.set_bind("", lambda e: self.to_right(name)) 44 | right.set_bind("", lambda e: self.to_end(name)) 45 | 46 | scroller.bind("", lambda e: self.to_left(name)) 47 | scroller.bind("", lambda e: self.to_right(name)) 48 | 49 | for child in scroller.winfo_children(): 50 | child.bind("", lambda e: self.to_left(name)) 51 | child.bind("", lambda e: self.to_right(name)) 52 | 53 | def to_left(self, name: str) -> None: 54 | from .widgets import widgets 55 | 56 | canvas = getattr(widgets, f"scroller_canvas_{name}") 57 | scroll_pos_left = canvas.xview()[0] 58 | 59 | if scroll_pos_left == 0.0: 60 | return 61 | 62 | canvas.xview_scroll(-widgets.canvas_scroll, "units") 63 | self.check_buttons(name) 64 | 65 | def to_right(self, name: str) -> None: 66 | from .widgets import widgets 67 | 68 | canvas = getattr(widgets, f"scroller_canvas_{name}") 69 | scroll_pos_right = canvas.xview()[1] 70 | 71 | if scroll_pos_right == 1.0: 72 | return 73 | 74 | canvas.xview_scroll(widgets.canvas_scroll, "units") 75 | self.check_buttons(name) 76 | 77 | def to_start(self, name: str) -> None: 78 | from .widgets import widgets 79 | 80 | canvas = getattr(widgets, f"scroller_canvas_{name}") 81 | canvas.xview_moveto(0) 82 | self.check_buttons(name) 83 | 84 | def to_end(self, name: str) -> None: 85 | from .widgets import widgets 86 | 87 | canvas = getattr(widgets, f"scroller_canvas_{name}") 88 | canvas.xview_moveto(1.0) 89 | self.check_buttons(name) 90 | 91 | def check_buttons(self, name: str) -> None: 92 | from .widgets import widgets 93 | from .tooltips import ToolTip 94 | 95 | canvas = getattr(widgets, f"scroller_canvas_{name}") 96 | scroll_pos_left = canvas.xview()[0] 97 | scroll_pos_right = canvas.xview()[1] 98 | ToolTip.hide_all() 99 | 100 | left = getattr(widgets, f"scroller_button_left_{name}") 101 | right = getattr(widgets, f"scroller_button_right_{name}") 102 | 103 | if scroll_pos_left == 0: 104 | left.set_style("disabled") 105 | else: 106 | left.set_style("alt") 107 | 108 | if scroll_pos_right == 1.0: 109 | right.set_style("disabled") 110 | else: 111 | right.set_style("alt") 112 | 113 | def check_all_buttons(self) -> None: 114 | self.check_buttons("system") 115 | self.check_buttons("details_1") 116 | self.check_buttons("details_2") 117 | 118 | def add(self, widgets: "Widgets", name: str) -> None: 119 | frame_data = widgetutils.make_frame() 120 | setattr(widgets, f"{name}_frame", frame_data.frame) 121 | left_frame = widgetutils.make_frame(frame_data.frame, col=0, row=0) 122 | left_frame.frame.grid_rowconfigure(0, weight=1) 123 | 124 | left_button = widgetutils.make_button( 125 | left_frame, self.left_icon, lambda: self.to_left(name), style="alt" 126 | ) 127 | 128 | setattr(widgets, f"scroller_button_left_{name}", left_button) 129 | ToolTip(left_button, tips["scroller_button"]) 130 | 131 | scroller_frame, canvas = widgetutils.make_scrollable_frame(frame_data.frame, 1) 132 | 133 | setattr(widgets, f"scroller_{name}", scroller_frame) 134 | setattr(widgets, f"scroller_canvas_{name}", canvas) 135 | right_frame = widgetutils.make_frame(frame_data.frame, col=2, row=0) 136 | right_frame.frame.grid_rowconfigure(0, weight=1) 137 | 138 | right_button = widgetutils.make_button( 139 | right_frame, 140 | self.right_icon, 141 | lambda: self.to_right(name), 142 | style="alt", 143 | ) 144 | 145 | setattr(widgets, f"scroller_button_right_{name}", right_button) 146 | ToolTip(right_button, tips["scroller_button"]) 147 | 148 | if not args.scroller_buttons: 149 | left_button.grid_remove() 150 | right_button.grid_remove() 151 | 152 | frame_data.frame.columnconfigure(1, weight=1) 153 | 154 | 155 | scrollers = Scrollers() 156 | -------------------------------------------------------------------------------- /meltdown/separatorbox.py: -------------------------------------------------------------------------------- 1 | # Standard 2 | import tkinter as tk 3 | 4 | # Modules 5 | from .app import app 6 | 7 | 8 | class SeparatorBox(tk.Frame): 9 | def __init__( 10 | self, parent: tk.Widget, background: str, padx: int, pady: int 11 | ) -> None: 12 | super().__init__(parent, background=background) 13 | line = tk.Frame(self, height=1, background=app.theme.separator_color) 14 | line.pack(fill="x", padx=padx, pady=pady) 15 | -------------------------------------------------------------------------------- /meltdown/signals.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | # Standard 4 | from pathlib import Path 5 | from typing import Any 6 | from http import HTTPStatus 7 | 8 | # Modules 9 | from .args import args 10 | from .utils import utils 11 | from .files import files 12 | from .display import display 13 | from .formats import formats 14 | 15 | # Libraries 16 | import requests # type: ignore 17 | 18 | 19 | class Signals: 20 | def __init__(self) -> None: 21 | self.timeout = 10 22 | 23 | def read_signals(self) -> Any | None: 24 | path = Path(args.signals) 25 | 26 | if not path.exists(): 27 | return None 28 | 29 | return files.load(path) 30 | 31 | def run(self, name: str) -> None: 32 | if not args.signals: 33 | display.print("Signals file path not set.") 34 | return 35 | 36 | messages = display.has_messages() 37 | ignored = display.is_ignored() 38 | 39 | if (not messages) or ignored: 40 | return 41 | 42 | try: 43 | signals = self.read_signals() 44 | except BaseException as e: 45 | utils.error(e) 46 | display.print("Can't read the signals file.") 47 | return 48 | 49 | if not signals: 50 | display.print("Signals file is empty.") 51 | return 52 | 53 | if name not in signals: 54 | display.print("That signal does not exist.") 55 | return 56 | 57 | signal = signals[name] 58 | required = ["url", "content"] 59 | 60 | if not all(key in signal for key in required): 61 | display.print("The signal is not configured properly.") 62 | return 63 | 64 | url = signal.get("url") 65 | content_key = signal.get("content") 66 | length = signal.get("length", 0) 67 | method = signal.get("method", "post") 68 | method_lower = method.lower() 69 | 70 | if method_lower not in ["post", "get", "put"]: 71 | display.print("Invalid method.") 72 | return 73 | 74 | items = signal.get("items", "all") 75 | format_ = signal.get("format", "json") 76 | single = signal.get("single", False) 77 | content = self.get_content(format_, items) 78 | 79 | if not content: 80 | display.print("No content to send.") 81 | return 82 | 83 | if single: 84 | content = content.replace("\n", " ").strip() 85 | 86 | if length > 0: 87 | content = content[:length].strip() 88 | 89 | data = signal.get("data", {}) 90 | 91 | for key, value in data.items(): 92 | data[key] = utils.replace_keywords(value) 93 | 94 | data[content_key] = content 95 | res: Any = None 96 | 97 | try: 98 | if method_lower == "get": 99 | res = requests.get(url, params=data, timeout=self.timeout) 100 | elif method_lower == "post": 101 | res = requests.post(url, data=data, timeout=self.timeout) 102 | elif method_lower == "put": 103 | res = requests.put(url, data=data, timeout=self.timeout) 104 | except requests.exceptions.RequestException as e: 105 | utils.error(e) 106 | display.print("Signal error.") 107 | return 108 | 109 | if not res: 110 | display.print("Signal error.") 111 | 112 | if res.status_code != HTTPStatus.OK: 113 | display.print("Signal error.") 114 | return 115 | 116 | display.print("Signal sent.") 117 | 118 | def get_content(self, format_: str = "json", mode: str = "all") -> str | None: 119 | tabconvo = display.get_tab_convo() 120 | 121 | if not tabconvo: 122 | return None 123 | 124 | if not tabconvo.convo.items: 125 | return None 126 | 127 | text = "" 128 | 129 | if format_ == "text": 130 | text = formats.get_text(tabconvo.convo, mode=mode) 131 | elif format_ == "json": 132 | text = formats.get_json(tabconvo.convo, mode=mode) 133 | elif format_ == "markdown": 134 | text = formats.get_markdown(tabconvo.convo, mode=mode) 135 | 136 | return text.strip() 137 | 138 | 139 | signals = Signals() 140 | -------------------------------------------------------------------------------- /meltdown/snippet.py: -------------------------------------------------------------------------------- 1 | # Standard 2 | import tkinter as tk 3 | from tkinter import ttk 4 | from typing import Any 5 | 6 | # Libraries 7 | from pygments.lexers import get_lexer_by_name # type: ignore 8 | from pygments.styles import get_style_by_name # type: ignore 9 | from pygments.util import ClassNotFound # type: ignore 10 | 11 | # Modules 12 | from .output import Output 13 | from .args import args 14 | from .app import app 15 | from .utils import utils 16 | from .gestures import Gestures 17 | from .model import model 18 | from .inputcontrol import inputcontrol 19 | from .dialogs import Dialog 20 | from .variables import variables 21 | 22 | 23 | class SnippetLabel(tk.Label): 24 | def __init__(self, parent: tk.Widget, text: str) -> None: 25 | super().__init__(parent, text=text) 26 | 27 | font = app.theme.get_output_font(True) 28 | fg_color = app.theme.snippet_header_foreground 29 | bg_color = app.theme.snippet_header_background 30 | 31 | self.configure(font=font) 32 | self.configure(cursor="arrow") 33 | self.pack(side=tk.LEFT, padx=5) 34 | self.configure(foreground=fg_color) 35 | self.configure(background=bg_color) 36 | 37 | 38 | class SnippetButton(tk.Label): 39 | def __init__(self, parent: tk.Widget, text: str) -> None: 40 | super().__init__(parent, text=text) 41 | 42 | font = app.theme.get_output_font(True) 43 | underline = app.theme.get_output_font(True, True) 44 | fg_color = app.theme.snippet_header_foreground 45 | bg_color = app.theme.snippet_header_background 46 | 47 | self.configure(font=font) 48 | self.configure(cursor="hand2") 49 | self.pack(side=tk.RIGHT, padx=5) 50 | self.configure(foreground=fg_color) 51 | self.configure(background=bg_color) 52 | 53 | def on_enter(event: Any) -> None: 54 | self.configure(font=underline) 55 | 56 | def on_leave(event: Any) -> None: 57 | self.configure(font=font) 58 | 59 | self.bind("", on_enter) 60 | self.bind("", on_leave) 61 | 62 | 63 | class Snippet(tk.Frame): 64 | def __init__(self, parent: Output, content: str, language: str) -> None: 65 | super().__init__(parent, borderwidth=0, highlightthickness=0) 66 | self.content = utils.untab_text(content) 67 | self.language = language 68 | 69 | self.header = tk.Frame(self) 70 | self.header.configure(background=app.theme.snippet_header_background) 71 | 72 | if language: 73 | header_text = f"Language: {language}" 74 | else: 75 | header_text = "Plain Text" 76 | 77 | self.header_text = SnippetLabel(self.header, header_text) 78 | self.header_copy = SnippetButton(self.header, "Copy") 79 | self.header_select = SnippetButton(self.header, "Select") 80 | self.header_find = SnippetButton(self.header, "Find") 81 | self.header_explain = SnippetButton(self.header, "Explain") 82 | self.header_view = SnippetButton(self.header, "View") 83 | self.header_sample = SnippetButton(self.header, "Use") 84 | 85 | self.header.pack(side=tk.TOP, fill=tk.X) 86 | self.text = tk.Text(self, wrap="none", state="normal") 87 | self.text.configure(borderwidth=0, highlightthickness=0) 88 | self.text.delete("1.0", tk.END) 89 | 90 | def insert() -> None: 91 | self.text.insert("1.0", self.content) 92 | 93 | if language and args.syntax_highlighting: 94 | try: 95 | if not self.syntax_highlighter(): 96 | insert() 97 | except BaseException as e: 98 | utils.error(e) 99 | insert() 100 | else: 101 | insert() 102 | 103 | self.text.configure(state="disabled") 104 | self.text.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=5, pady=5) 105 | 106 | self.scrollbar = ttk.Scrollbar( 107 | self, style="Normal.Horizontal.TScrollbar", orient=tk.HORIZONTAL 108 | ) 109 | 110 | self.scrollbar.configure(cursor="arrow") 111 | self.text.configure(xscrollcommand=self.scrollbar.set) 112 | 113 | self.text.tag_configure( 114 | "sel", 115 | background=app.theme.snippet_selection_background, 116 | foreground=app.theme.snippet_selection_foreground, 117 | ) 118 | 119 | if args.scrollbars: 120 | self.scrollbar.pack(fill=tk.X) 121 | 122 | self.parent = parent 123 | 124 | num_lines = int(self.text.index("end-1c").split(".")[0]) 125 | self.text.configure(height=num_lines) 126 | 127 | self.configure(background=app.theme.snippet_background) 128 | self.text.configure(background=app.theme.snippet_background) 129 | self.text.configure(foreground=app.theme.snippet_foreground) 130 | self.text.configure(font=app.theme.get_snippet_font()) 131 | self.scrollbar.configure(command=self.text.xview) 132 | 133 | self.update_size() 134 | self.setup_bindings() 135 | 136 | def setup_bindings(self) -> None: 137 | def bind_scroll_events(widget: tk.Widget) -> None: 138 | widget.bind("", lambda e: self.on_click()) 139 | widget.bind("", lambda e: self.scroll_up()) 140 | widget.bind("", lambda e: self.scroll_down()) 141 | widget.bind("", lambda e: self.scroll_left()) 142 | widget.bind("", lambda e: self.scroll_right()) 143 | widget.bind("", lambda e: self.parent.increase_font()) 144 | widget.bind("", lambda e: self.parent.decrease_font()) 145 | 146 | for child in widget.winfo_children(): 147 | bind_scroll_events(child) 148 | 149 | bind_scroll_events(self) 150 | 151 | self.header_copy.bind("", lambda e: self.copy_all()) 152 | self.header_explain.bind("", lambda e: self.explain()) 153 | self.header_sample.bind("", lambda e: self.sample_variable()) 154 | self.header_view.bind("", lambda e: self.view_text()) 155 | self.header_select.bind("", lambda e: self.select_all()) 156 | self.header_find.bind("", lambda e: self.find()) 157 | self.text.bind("", lambda e: self.on_motion(e)) 158 | self.gestures = Gestures(self, self.text, self.on_right_click) 159 | 160 | def update_size(self) -> None: 161 | try: 162 | char_width = self.text.tk.call( 163 | "font", "measure", self.text.cget("font"), "0" 164 | ) 165 | except BaseException: 166 | return 167 | 168 | width_pixels = self.parent.winfo_width() - self.parent.scrollbar.winfo_width() 169 | width_pixels = int(width_pixels * 0.98) 170 | width_chars = int(width_pixels / char_width) 171 | self.text.configure(width=width_chars) 172 | 173 | def update_font(self) -> None: 174 | font_header = app.theme.get_output_font(True) 175 | snippet_font = app.theme.get_snippet_font() 176 | self.text.configure(font=snippet_font) 177 | self.header_text.configure(font=font_header) 178 | self.header_copy.configure(font=font_header) 179 | self.header_select.configure(font=font_header) 180 | self.update_size() 181 | 182 | def overflowed(self) -> bool: 183 | pos = self.scrollbar.get() 184 | return pos[0] != 0.0 or pos[1] != 1.0 185 | 186 | def scroll_left(self) -> str: 187 | if not self.overflowed(): 188 | self.parent.tab_left() 189 | return "break" 190 | 191 | self.text.xview_scroll(-2, "units") 192 | return "break" 193 | 194 | def scroll_right(self) -> str: 195 | if not self.overflowed(): 196 | self.parent.tab_right() 197 | return "break" 198 | 199 | self.text.xview_scroll(2, "units") 200 | return "break" 201 | 202 | def copy_all(self) -> None: 203 | app.hide_all() 204 | self.deselect_all() 205 | utils.copy(self.content) 206 | self.header_copy.configure(text="Copied!") 207 | self.after(1000, lambda: self.header_copy.configure(text="Copy")) 208 | 209 | def select_all(self) -> None: 210 | app.hide_all() 211 | self.text.tag_add("sel", "1.0", tk.END) 212 | 213 | def deselect_all(self) -> None: 214 | if not self.text: 215 | return 216 | 217 | self.text.tag_remove("sel", "1.0", tk.END) 218 | 219 | def get_sample(self) -> str: 220 | sample = self.content[0 : args.explain_sample] 221 | return sample.replace("\n", " ").strip() 222 | 223 | def explain(self) -> None: 224 | sample = self.get_sample() 225 | text = f"Explain this snippet: {sample}" 226 | model.stream({"text": text}, self.parent.tab_id) 227 | 228 | def sample_variable(self) -> None: 229 | sample = self.get_sample() 230 | variables.do_set_variable("snippet", sample, feedback=False) 231 | v = variables.varname("snippet") 232 | inputcontrol.set(v) 233 | 234 | def on_motion(self, event: Any) -> None: 235 | current_index = self.text.index(tk.CURRENT) 236 | 237 | Output.words = self.text.get( 238 | f"{current_index} wordstart", f"{current_index} wordend" 239 | ) 240 | 241 | def get_selected_text(self) -> str: 242 | try: 243 | start = self.text.index(tk.SEL_FIRST) 244 | end = self.text.index(tk.SEL_LAST) 245 | return self.text.get(start, end) 246 | except tk.TclError: 247 | return "" 248 | 249 | def syntax_highlighter(self) -> bool: 250 | try: 251 | lexer = get_lexer_by_name(self.language, stripall=True) 252 | except ClassNotFound: 253 | return False 254 | 255 | style = get_style_by_name(app.theme.syntax_style) 256 | parsed = style.list_styles() 257 | 258 | for key in parsed: 259 | if key[1]["color"] != "" and key[1]["color"] is not None: 260 | color = "#" + key[1]["color"] 261 | key_ = str(key[0]) 262 | self.text.tag_configure(key_, foreground=color) 263 | self.text.tag_lower(key_) 264 | 265 | tokens = list(lexer.get_tokens(self.content)) 266 | 267 | for text in tokens: 268 | self.text.insert(tk.END, text[1], str(text[0])) 269 | 270 | last_line_index = self.text.index("end-1c linestart") 271 | last_line_text = self.text.get(last_line_index, "end-1c") 272 | 273 | if not last_line_text.strip(): 274 | self.text.delete(last_line_index, "end") 275 | 276 | return True 277 | 278 | def on_click(self) -> None: 279 | app.hide_all() 280 | self.parent.deselect_all() 281 | self.parent.display.unpick() 282 | 283 | def scroll_up(self) -> str: 284 | self.parent.scroll_up(True) 285 | return "break" 286 | 287 | def scroll_down(self) -> str: 288 | self.parent.scroll_down(True) 289 | return "break" 290 | 291 | def find(self) -> None: 292 | from .findmanager import findmanager 293 | 294 | app.hide_all() 295 | findmanager.find(tab_id=self.parent.tab_id, widget=self.text) 296 | 297 | def on_right_click(self, event: Any) -> None: 298 | self.parent.on_right_click(event, self.text) 299 | 300 | def view_text(self) -> None: 301 | text = self.content 302 | text = utils.remove_multiple_lines(text) 303 | 304 | def cmd_ok(text: str) -> None: 305 | pass 306 | 307 | Dialog.show_textbox("snippet_view", "Raw Text", cmd_ok, value=text) 308 | -------------------------------------------------------------------------------- /meltdown/summarize.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | # Modules 4 | from .args import args 5 | from .display import display 6 | from .model import model 7 | from .utils import utils 8 | from .formats import formats 9 | 10 | 11 | class Summarize: 12 | def summarize(self, tab_id: str | None = None) -> None: 13 | tabconvo = display.get_tab_convo(tab_id) 14 | 15 | if not tabconvo: 16 | return 17 | 18 | text = formats.get_text(tabconvo.convo, "minimal") 19 | 20 | if not text: 21 | text = display.get_text(tab_id) 22 | 23 | if not text: 24 | return 25 | 26 | prompt = {} 27 | 28 | sumprompt = args.summarize_prompt 29 | sumprompt = utils.replace_keywords(sumprompt) 30 | 31 | prompt["user"] = "Please summarize this." 32 | prompt["text"] = f"{sumprompt}: " 33 | prompt["text"] += text 34 | 35 | tab_id = display.make_tab() 36 | 37 | if not tab_id: 38 | return 39 | 40 | model.stream(prompt, tab_id=tab_id) 41 | 42 | 43 | summarize = Summarize() 44 | -------------------------------------------------------------------------------- /meltdown/system.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | # Standard 4 | import json 5 | import subprocess 6 | import threading 7 | import tkinter as tk 8 | from pathlib import Path 9 | 10 | # Libraries 11 | import psutil # type: ignore 12 | 13 | # Modules 14 | from .widgets import widgets 15 | from .args import args 16 | from .app import app 17 | from .utils import utils 18 | from .framedata import FrameData 19 | from .tooltips import ToolTip 20 | from .tips import tips 21 | from .model import model 22 | from .widgetutils import widgetutils 23 | 24 | 25 | class System: 26 | def __init__(self) -> None: 27 | self.cpu: int | None = None 28 | self.ram: int | None = None 29 | self.temp: int | None = None 30 | self.gpu_use: int | None = None 31 | self.gpu_ram: int | None = None 32 | self.gpu_temp: int | None = None 33 | self.clean = True 34 | 35 | def set_widget(self, widget: tk.StringVar, text: str) -> None: 36 | if app.exists(): 37 | widget.set(text) 38 | 39 | def get_psutil_info(self) -> None: 40 | if args.system_cpu: 41 | self.cpu = int(psutil.cpu_percent(interval=1)) 42 | self.set_widget(widgets.cpu, utils.padnum(self.cpu) + "%") 43 | 44 | if args.system_ram: 45 | self.ram = int(psutil.virtual_memory().percent) 46 | self.set_widget(widgets.ram, utils.padnum(self.ram) + "%") 47 | 48 | if args.system_temp: 49 | temps = psutil.sensors_temperatures() 50 | ktemps = temps.get("k10temp") 51 | 52 | if ktemps: 53 | for ktemp in ktemps: 54 | # This one works with AMD Ryzen 55 | if ktemp.label == "Tctl": 56 | self.temp = int(ktemp.current) 57 | break 58 | 59 | if self.temp: 60 | self.set_widget(widgets.temp, utils.padnum(self.temp) + "°") 61 | else: 62 | self.set_widget(widgets.temp, "N/A") 63 | 64 | def get_gpu_info(self) -> None: 65 | # This works with AMD GPUs | rocm-smi must be installed 66 | if args.system_gpu or args.system_gpu_ram or args.system_gpu_temp: 67 | rocm_smi = "/opt/rocm/bin/rocm-smi" 68 | 69 | if not Path(rocm_smi).is_file(): 70 | return 71 | 72 | cmd = [ 73 | rocm_smi, 74 | "--showtemp", 75 | "--showuse", 76 | "--showmemuse", 77 | "--json", 78 | ] 79 | 80 | result = None 81 | 82 | try: 83 | result = subprocess.run( 84 | cmd, capture_output=True, text=True, check=False 85 | ) 86 | except BaseException: 87 | return 88 | 89 | if result and result.returncode == 0: 90 | ans = json.loads(result.stdout) 91 | 92 | if "card0" in ans: 93 | gpu_data = ans["card0"] 94 | 95 | if args.system_gpu: 96 | self.gpu_use = int(float(gpu_data.get("GPU use (%)", 0))) 97 | 98 | self.set_widget( 99 | widgets.gpu, utils.padnum(int(self.gpu_use)) + "%" 100 | ) 101 | 102 | if args.system_gpu_ram: 103 | self.gpu_ram = int( 104 | float(gpu_data.get("GPU Memory Allocated (VRAM%)", 0)) 105 | ) 106 | 107 | self.set_widget( 108 | widgets.gpu_ram, utils.padnum(int(self.gpu_ram)) + "%" 109 | ) 110 | 111 | if args.system_gpu_temp: 112 | self.gpu_temp = int( 113 | float(gpu_data.get("Temperature (Sensor junction) (C)", 0)) 114 | ) 115 | 116 | self.set_widget( 117 | widgets.gpu_temp, utils.padnum(int(self.gpu_temp)) + "°C" 118 | ) 119 | 120 | def get_info(self) -> None: 121 | self.get_psutil_info() 122 | self.get_gpu_info() 123 | 124 | if args.system_colors: 125 | self.set_colors() 126 | 127 | self.clean = False 128 | 129 | def set_colors(self) -> None: 130 | if self.cpu is not None: 131 | self.check_color("cpu", self.cpu) 132 | 133 | if self.ram is not None: 134 | self.check_color("ram", self.ram) 135 | 136 | if self.temp is not None: 137 | self.check_color("temp", self.temp) 138 | 139 | if self.gpu_use is not None: 140 | self.check_color("gpu", self.gpu_use) 141 | 142 | if self.gpu_ram is not None: 143 | self.check_color("gpu_ram", self.gpu_ram) 144 | 145 | if self.gpu_temp is not None: 146 | self.check_color("gpu_temp", self.gpu_temp) 147 | 148 | def check_color(self, name: str, var: int, reset: bool = False) -> None: 149 | if getattr(args, f"system_{name}"): 150 | label = getattr(widgets, f"{name}_text") 151 | 152 | if reset: 153 | label.configure(foreground=app.theme.foreground_color) 154 | elif var >= args.system_threshold: 155 | label.configure(foreground=app.theme.system_heavy) 156 | else: 157 | label.configure(foreground=app.theme.system_normal) 158 | 159 | def start_loop(self) -> None: 160 | utils.sleep(1) 161 | o_check = True 162 | 163 | while True: 164 | if app.system_frame_enabled: 165 | check = True 166 | 167 | if (args.system_suspend >= 1) and (not model.streaming): 168 | date = model.stream_date 169 | 170 | if not date: 171 | check = False 172 | else: 173 | mins = (utils.now() - date) / 60 174 | 175 | if mins >= args.system_suspend: 176 | check = False 177 | 178 | changed = check != o_check 179 | o_check = check 180 | 181 | if check: 182 | try: 183 | self.get_info() 184 | except BaseException as e: 185 | utils.error(e) 186 | elif changed: 187 | self.reset() 188 | 189 | utils.sleep(args.system_delay) 190 | 191 | def start(self) -> None: 192 | if args.system_delay < 1: 193 | return 194 | 195 | self.check_auto_hide() 196 | thread = threading.Thread(target=lambda: self.start_loop()) 197 | thread.daemon = True 198 | thread.start() 199 | 200 | def reset(self) -> None: 201 | if self.clean: 202 | return 203 | 204 | self.cpu = None 205 | self.ram = None 206 | self.temp = None 207 | self.gpu_use = None 208 | self.gpu_ram = None 209 | self.gpu_temp = None 210 | 211 | text = "000%" 212 | 213 | self.set_widget(widgets.cpu, text) 214 | self.set_widget(widgets.ram, text) 215 | self.set_widget(widgets.temp, text) 216 | self.set_widget(widgets.gpu, text) 217 | self.set_widget(widgets.gpu_ram, text) 218 | self.set_widget(widgets.gpu_temp, text) 219 | 220 | self.check_color("cpu", 0, True) 221 | self.check_color("ram", 0, True) 222 | self.check_color("temp", 0, True) 223 | self.check_color("gpu", 0, True) 224 | self.check_color("gpu_ram", 0, True) 225 | self.check_color("gpu_temp", 0, True) 226 | 227 | self.clean = True 228 | 229 | def add_items(self) -> None: 230 | data = FrameData(widgets.scroller_system) 231 | first = False 232 | 233 | def make_monitor(name: str, label_text: str, mode: str) -> None: 234 | nonlocal first 235 | 236 | if not first: 237 | padx = (0, 0) 238 | first = True 239 | else: 240 | padx = (app.theme.padx, 0) 241 | 242 | label = widgetutils.make_label( 243 | data, label_text, ignore_short=(not args.short_system), padx=padx 244 | ) 245 | 246 | label.configure(cursor="hand2") 247 | setattr(widgets, name, tk.StringVar()) 248 | monitor_text = widgetutils.make_label(data, "", padx=(0, app.theme.padx)) 249 | monitor_text.configure(textvariable=getattr(widgets, name)) 250 | monitor_text.configure(cursor="hand2") 251 | setattr(widgets, f"{name}_text", monitor_text) 252 | tip = tips[f"system_{name}"] 253 | ToolTip(label, tip) 254 | ToolTip(monitor_text, tip) 255 | getattr(widgets, name).set("000%") 256 | 257 | label.bind("", lambda e: app.open_task_manager(mode)) 258 | monitor_text.bind("", lambda e: app.open_task_manager(mode)) 259 | 260 | if args.system_cpu: 261 | make_monitor("cpu", "CPU", "normal") 262 | 263 | if args.system_ram: 264 | make_monitor("ram", "RAM", "normal") 265 | 266 | if args.system_temp: 267 | make_monitor("temp", "TMP", "normal") 268 | 269 | if args.system_gpu: 270 | make_monitor("gpu", "GPU", "gpu") 271 | 272 | if args.system_gpu_ram: 273 | make_monitor("gpu_ram", "GPU RAM", "gpu") 274 | 275 | if args.system_gpu_temp: 276 | make_monitor("gpu_temp", "GPU TMP", "gpu") 277 | 278 | def check_auto_hide(self) -> None: 279 | if args.system_auto_hide: 280 | if model.loaded_type == "local": 281 | app.show_frame("system") 282 | else: 283 | app.hide_frame("system") 284 | 285 | 286 | system = System() 287 | -------------------------------------------------------------------------------- /meltdown/system_prompt.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | # Standard 4 | from typing import Any 5 | 6 | # Modules 7 | from .args import args 8 | from .config import config 9 | from .utils import utils 10 | from .menus import Menu 11 | from .dialogs import Dialog 12 | from .files import files 13 | from .textbox import TextBox 14 | 15 | 16 | class SystemPrompt: 17 | def write(self, text: str | None = None, maxed: bool = False) -> None: 18 | if text: 19 | config.set("system", text) 20 | return 21 | 22 | Dialog.show_textbox( 23 | "system", 24 | "System Prompt", 25 | lambda a: self.action(a), 26 | value=config.system, 27 | start_maximized=maxed, 28 | on_right_click=self.on_right_click, 29 | ) 30 | 31 | def action(self, ans: dict[str, Any]) -> None: 32 | config.set("system", ans["text"]) 33 | 34 | def on_right_click(self, event: Any, textbox: TextBox) -> None: 35 | menu = Menu() 36 | text = textbox.get_text() 37 | menu.add(text="Copy", command=lambda e: textbox.copy()) 38 | menu.add(text="Paste", command=lambda e: textbox.paste()) 39 | 40 | if text: 41 | menu.add(text="Clear", command=lambda e: textbox.clear()) 42 | 43 | if text != config.get_default("system"): 44 | menu.add(text="Reset", command=lambda e: self.reset(textbox)) 45 | 46 | items = files.get_list("systems")[: args.max_list_items] 47 | 48 | if items: 49 | 50 | def cmd(s: str) -> None: 51 | config.set("system", s) 52 | textbox.set_text(s) 53 | textbox.focus_end() 54 | 55 | def forget(s: str) -> None: 56 | files.remove_system(s) 57 | self.on_right_click(event, textbox) 58 | 59 | utils.fill_recent(menu, items, text, cmd, alt_cmd=lambda s: forget(s)) 60 | 61 | menu.show(event) 62 | 63 | def reset(self, textbox: TextBox) -> None: 64 | value = config.get_default("system") 65 | 66 | if value: 67 | textbox.set_text(value) 68 | config.reset_one("system") 69 | 70 | textbox.focus_end() 71 | 72 | 73 | system_prompt = SystemPrompt() 74 | -------------------------------------------------------------------------------- /meltdown/tasks.py: -------------------------------------------------------------------------------- 1 | # Standard 2 | import re 3 | import threading 4 | 5 | # Modules 6 | from .args import args 7 | from .commands import commands 8 | from .utils import utils 9 | 10 | 11 | class Task: 12 | prefix = utils.escape_regex(args.command_prefix) 13 | pattern = rf"^((?P