Skip to content

Commit 0ff6997

Browse files
committed
Release 1.2.0 "Extensia"
1 parent 9e4f225 commit 0ff6997

File tree

1 file changed

+118
-15
lines changed

1 file changed

+118
-15
lines changed

main.py

Lines changed: 118 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,26 @@
55
from tkinter import simpledialog
66
import time, threading
77
import os, json, platform
8-
import markdown
9-
from tkinterweb import HtmlFrame
10-
118

129
# ██╗░░██╗██████╗░░█████╗░██████╗░
1310
# ██║░██╔╝██╔══██╗██╔══██╗██╔══██╗
1411
# █████═╝░██████╔╝███████║██║░░██║
1512
# ██╔═██╗░██╔═══╝░██╔══██║██║░░██║
1613
# ██║░╚██╗██║░░░░░██║░░██║██████╔╝
1714
# ╚═╝░░╚═╝╚═╝░░░░░╚═╝░░╚═╝╚═════╝░
18-
# Version 1.1.0 [SOURCE CODE]
15+
# Version 1.2.0 [SOURCE CODE]
1916

2017
if platform.system() == 'Darwin':
2118
config_dir = os.path.expanduser('~/Library/Application Support/kPad')
19+
plugin_dir = os.path.expanduser(f'{config_dir}/plugins')
2220
elif platform.system() == 'Windows':
2321
config_dir = os.path.join(os.getenv('APPDATA'), 'kPad')
22+
plugin_dir = os.path.join(os.getenv('APPDATA'), 'kPad', 'Plugins')
2423
else:
2524
config_dir = os.path.expanduser('~/.config/kpad')
25+
plugin_dir = os.path.expanduser(f'{config_dir}/plugins')
2626
os.makedirs(config_dir, exist_ok=True)
27+
os.makedirs(plugin_dir, exist_ok=True)
2728
CONFIG_PATH = os.path.join(config_dir, 'config.json')
2829
if os.path.exists(CONFIG_PATH):
2930
with open(CONFIG_PATH, 'r') as f:
@@ -41,10 +42,89 @@
4142

4243
_fonts = ['Menlo', 'Monaco', 'Helvetica', 'Arial', 'Times New Roman', 'Georgia', 'Avenir', 'Baskerville', 'Futura', 'Verdana', 'Gill Sans', 'Courier', 'Optima', 'American Typewriter']
4344

45+
class PluginAPI:
46+
def __init__(self, textbox, appinstance):
47+
self.textbox = textbox
48+
self._appinstance = appinstance
49+
def get_text_from_box(self):
50+
return self.textbox.get("1.0", "end-1c")
51+
def get_specific_text_from_box(self, start, end):
52+
return self.textbox.get(start, end)
53+
def clear_text_from_box(self):
54+
self.textbox.delete("1.0", "end")
55+
def insert_text_to_start_of_box(self, text):
56+
self.textbox.insert("1.0", text)
57+
def insert_text_to_end_of_box(self, text):
58+
self.textbox.insert("end", text)
59+
def bind(self, sequence, callback):
60+
def wrapper(event):
61+
try:
62+
callback(event)
63+
except TypeError:
64+
callback()
65+
return "break"
66+
self.textbox.bind(sequence, wrapper)
67+
def get_plugin_path(self, plugin_name):
68+
return os.path.join(plugin_dir, plugin_name)
69+
def get_current_file_path(self):
70+
return self._appinstance.path
71+
def get_current_theme_mode(self):
72+
return ctk.get_appearance_mode()
73+
def set_current_theme_mode(self, mode):
74+
if mode == 'light':
75+
ctk.set_appearance_mode('light')
76+
else:
77+
ctk.set_appearance_mode('dark')
78+
def set_theme_file(self, json_path):
79+
if os.path.exists(json_path):
80+
ctk.set_default_color_theme(json_path)
81+
def show_info(self, text):
82+
showinfo("Info", text)
83+
84+
def show_error(self, text):
85+
showinfo("Error", text)
86+
87+
def log(self, text):
88+
print(f"[PLUGIN LOG] {text}")
89+
90+
4491
class App(ctk.CTk):
4592
def __init__(self, title, geometry):
4693
super().__init__()
4794

95+
import importlib.util
96+
97+
self.textbox = ctk.CTkTextbox(self, undo=CONFIGURATION['undo']['enabled'], autoseparators=CONFIGURATION['undo']['separate_edits_from_undos'], maxundo=CONFIGURATION['undo']['max_undo'])
98+
99+
def __load_plugins():
100+
plugins = []
101+
if not os.path.exists(plugin_dir):
102+
os.makedirs(plugin_dir)
103+
for folder in os.listdir(plugin_dir):
104+
folder_path = os.path.join(plugin_dir, folder)
105+
if not os.path.isdir(folder_path):
106+
continue
107+
logic_path = os.path.join(folder_path, "logic.py")
108+
if os.path.exists(logic_path):
109+
name = folder
110+
try:
111+
spec = importlib.util.spec_from_file_location(name, logic_path)
112+
mod = importlib.util.module_from_spec(spec)
113+
spec.loader.exec_module(mod)
114+
meta_path = os.path.join(folder_path, "metadata.json")
115+
metadata = None
116+
if os.path.exists(meta_path):
117+
with open(meta_path, "r") as f:
118+
metadata = json.load(f)
119+
if hasattr(mod, "action"):
120+
plugins.append({"module": mod, "meta": metadata})
121+
except Exception as e:
122+
print(f"[PLUGIN ERROR] Failed to load '{name}': {e}")
123+
124+
return plugins
125+
126+
PLUGINS_LIST = __load_plugins()
127+
48128
self.path = None
49129
self.font_size = 14
50130

@@ -167,20 +247,36 @@ def auto_indent(event):
167247
tb = event.widget
168248
cursor_index = tb.index("insert")
169249
line_number = int(cursor_index.split('.')[0])
170-
if line_number > 1:
171-
prev_line = tb.get(f"{line_number-1}.0", f"{line_number-1}.end")
172-
indent = len(prev_line) - len(prev_line.lstrip(' '))
173-
if prev_line.strip().endswith(":") or prev_line.strip().endswith("{"):
174-
indent += 4
250+
indent = 0
175251
prev_line_num = line_number - 1
176252
while prev_line_num > 0:
177253
prev_line_text = tb.get(f"{prev_line_num}.0", f"{prev_line_num}.end")
178254
if prev_line_text.strip() != "":
255+
indent = len(prev_line_text) - len(prev_line_text.lstrip(' '))
256+
if prev_line_text.rstrip().endswith((':', '{')):
257+
indent += 4
179258
break
180259
prev_line_num -= 1
181-
indent = len(prev_line_text) - len(prev_line_text.lstrip(' '))
182-
tb.insert("insert", "\n" + " " * indent)
183-
return "break"
260+
current_line_text = tb.get(f"{line_number}.0", f"{line_number}.end")
261+
if current_line_text.strip() == "":
262+
tb.insert("insert", " " * indent)
263+
tb.insert("insert", "\n" + " " * indent)
264+
return "break"
265+
266+
def add_second_char(char, event=None):
267+
def insert_char():
268+
cursor = self.textbox.index("insert")
269+
if char == '{':
270+
self.textbox.insert(cursor, '}')
271+
elif char == '[':
272+
self.textbox.insert(cursor, ']')
273+
elif char == '(':
274+
self.textbox.insert(cursor, ')')
275+
self.after(1, insert_char)
276+
277+
def handle_brackets(event):
278+
if event.char in '{[(':
279+
add_second_char(event.char)
184280

185281
menu = Menu(self)
186282
file_menu = Menu(menu, tearoff=0)
@@ -205,6 +301,15 @@ def auto_indent(event):
205301
view_menu.add_separator()
206302
view_menu.add_checkbutton(label='Word Wrap...', onvalue=True, variable=word_wrap_var, command=toggle_word_wrap)
207303
menu.add_cascade(label='View', menu=view_menu)
304+
self.plugins_menu = Menu(menu, tearoff=0)
305+
menu.add_cascade(label="Plugins", menu=self.plugins_menu)
306+
307+
editor_api = PluginAPI(self.textbox, self)
308+
self.plugins_list = PLUGINS_LIST
309+
for plugin in self.plugins_list:
310+
meta = plugin["meta"]
311+
name = meta.get("name") if meta else plugin["module"].__name__
312+
self.plugins_menu.add_command(label=name, command=lambda p=plugin: p["module"].action(editor_api))
208313

209314
self.configure(menu=menu)
210315

@@ -224,7 +329,6 @@ def update_cursor_info(event=None):
224329
self.title(title)
225330
self.geometry(f'{geometry[0]}x{geometry[1]}')
226331

227-
self.textbox = ctk.CTkTextbox(self, undo=CONFIGURATION['undo']['enabled'], autoseparators=CONFIGURATION['undo']['separate_edits_from_undos'], maxundo=CONFIGURATION['undo']['max_undo'])
228332
self.textbox.configure(font=self.font)
229333
if word_wrap_var.get():
230334
self.textbox.configure(wrap='word')
@@ -250,16 +354,15 @@ def update_cursor_info(event=None):
250354
self.bind(f'<{mod}-equal>', increment_font_size)
251355
self.bind(f'<{mod}-minus>', decrement_font_size)
252356

357+
self.textbox.bind('<Key>', handle_brackets)
253358

254359
self.stats_text_frame = ctk.CTkFrame(self)
255360
self.stats_text_frame.pack(fill='x', side=ctk.BOTTOM)
256361

257362
self.stats_line_col = ctk.CTkLabel(self.stats_text_frame, text='Ln: 1 Col: 1 Ch: 0')
258363
self.stats_line_col.pack(side=ctk.RIGHT)
259364

260-
self.html = markdown.markdown()
261365
self.textbox.pack(fill='both', expand=True)
262-
self.html_frame.pack(fill='both', expand=True)
263366
self.protocol('WM_DELETE_WINDOW', lambda: [save_file(), save_config(), self.destroy()])
264367

265368

0 commit comments

Comments
 (0)