55from tkinter import simpledialog
66import time , threading
77import 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
2017if platform .system () == 'Darwin' :
2118 config_dir = os .path .expanduser ('~/Library/Application Support/kPad' )
19+ plugin_dir = os .path .expanduser (f'{ config_dir } /plugins' )
2220elif platform .system () == 'Windows' :
2321 config_dir = os .path .join (os .getenv ('APPDATA' ), 'kPad' )
22+ plugin_dir = os .path .join (os .getenv ('APPDATA' ), 'kPad' , 'Plugins' )
2423else :
2524 config_dir = os .path .expanduser ('~/.config/kpad' )
25+ plugin_dir = os .path .expanduser (f'{ config_dir } /plugins' )
2626os .makedirs (config_dir , exist_ok = True )
27+ os .makedirs (plugin_dir , exist_ok = True )
2728CONFIG_PATH = os .path .join (config_dir , 'config.json' )
2829if os .path .exists (CONFIG_PATH ):
2930 with open (CONFIG_PATH , 'r' ) as f :
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+
4491class 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