44from tkinter .messagebox import showinfo , askyesno
55from tkinter import simpledialog
66import time , threading
7-
8- CONFIGURATION = {
9- 'auto_save' : {
10- 'enabled' : True ,
11- 'time_until_next_save' : 5 , # seconds
12- },
13- 'undo' : {
14- 'enabled' : True ,
15- 'max_undo' : 20 , # use -1 for infinite
16- 'separate_edits_from_undos' : True
17- }
18- }
7+ import os , json , platform
8+
9+
10+ # ██╗░░██╗██████╗░░█████╗░██████╗░
11+ # ██║░██╔╝██╔══██╗██╔══██╗██╔══██╗
12+ # █████═╝░██████╔╝███████║██║░░██║
13+ # ██╔═██╗░██╔═══╝░██╔══██║██║░░██║
14+ # ██║░╚██╗██║░░░░░██║░░██║██████╔╝
15+ # ╚═╝░░╚═╝╚═╝░░░░░╚═╝░░╚═╝╚═════╝░
16+ # Version 1.1.0 [SOURCE CODE]
17+
18+ if platform .system () == 'Darwin' :
19+ config_dir = os .path .expanduser ('~/Library/Application Support/kPad' )
20+ elif platform .system () == 'Windows' :
21+ config_dir = os .path .join (os .getenv ('APPDATA' ), 'kPad' )
22+ else :
23+ config_dir = os .path .expanduser ('~/.config/kpad' )
24+ os .makedirs (config_dir , exist_ok = True )
25+ CONFIG_PATH = os .path .join (config_dir , 'config.json' )
26+ if os .path .exists (CONFIG_PATH ):
27+ with open (CONFIG_PATH , 'r' ) as f :
28+ CONFIGURATION = json .load (f )
29+ else :
30+ CONFIGURATION = {
31+ 'auto_save' : {'enabled' : True , 'time_until_next_save' : 5 },
32+ 'undo' : {'enabled' : True , 'max_undo' : 20 , 'separate_edits_from_undos' : True },
33+ 'word_wrap' : True ,
34+ 'font' : 'Menlo' ,
35+ 'font_size' : 14 ,
36+ 'window_geometry' : [500 , 400 ],
37+ 'recent_files' : {'enabled' : True , 'keep_recent_files_count' : 5 , 'recent_file_paths' : []}
38+ }
1939
2040_fonts = ['Menlo' , 'Monaco' , 'Helvetica' , 'Arial' , 'Times New Roman' , 'Georgia' , 'Avenir' , 'Baskerville' , 'Futura' , 'Verdana' , 'Gill Sans' , 'Courier' , 'Optima' , 'American Typewriter' ]
2141
@@ -26,6 +46,15 @@ def __init__(self, title, geometry):
2646 self .path = None
2747 self .font_size = 14
2848
49+ def write_to_recent_files ():
50+ if CONFIGURATION ['recent_files' ]['enabled' ]:
51+ if len (CONFIGURATION ['recent_files' ]['recent_file_paths' ]) >= CONFIGURATION ['recent_files' ]['keep_recent_files_count' ]:
52+ del CONFIGURATION ['recent_files' ]['recent_file_paths' ][0 ]
53+ CONFIGURATION ['recent_files' ]['recent_file_paths' ].append (self .path )
54+ else :
55+ CONFIGURATION ['recent_files' ]['recent_file_paths' ].append (self .path )
56+
57+
2958 def newfile (event = None ):
3059 if not '*' in self .title ()[7 ]:
3160 self .title ('kPad - Untitled' )
@@ -55,12 +84,15 @@ def save_file(event=None):
5584 self .title (f'kPad - { self .path } ' )
5685
5786
58- def open_from_file (event = None ):
59- self .path = askopenfilename (filetypes = [('Text files' , '.txt' ), ('kPad notefile' , '.kpad' )], defaultextension = '.txt' )
87+ def open_from_file (event = None , path = None ):
88+ if not path :
89+ self .path = askopenfilename (filetypes = [('Text files' , '.txt' ), ('kPad notefile' , '.kpad' )], defaultextension = '.txt' )
6090 if self .path :
6191 self .textbox .delete ('1.0' , 'end' )
62- with open (self .path , "r" ) as file :
92+ with open (self .path , 'r' ) as file :
6393 self .textbox .insert ('1.0' , file .read ())
94+ write_to_recent_files ()
95+ print (CONFIGURATION ['recent_files' ]['recent_file_paths' ])
6496 else :
6597 pass
6698
@@ -79,7 +111,7 @@ def autosave():
79111
80112 def toggle_theme (event = None ):
81113 mode = ctk .get_appearance_mode ()
82- ctk .set_appearance_mode (" Light" if mode == " Dark" else " Dark" )
114+ ctk .set_appearance_mode (' Light' if mode == ' Dark' else ' Dark' )
83115
84116 def go_to_start (event = None ):
85117 self .textbox .yview_moveto (0 )
@@ -91,34 +123,55 @@ def increment_font_size(event=None):
91123 self .font_size += 2
92124 self .font = ctk .CTkFont (family = self .font ._family , size = self .font_size )
93125 self .textbox .configure (font = self .font )
94- return " break"
126+ return ' break'
95127
96128 def decrement_font_size (event = None ):
97129 self .font_size = max (2 , self .font_size - 2 )
98130 self .font = ctk .CTkFont (family = self .font ._family , size = self .font_size )
99131 self .textbox .configure (font = self .font )
100- return " break"
132+ return ' break'
101133
102134 def go_to_line (event = None ):
103- line = simpledialog .askinteger (" Go To Line" , " Enter line number:" )
135+ line = simpledialog .askinteger (' Go To Line' , ' Enter line number:' )
104136 if line is None :
105137 return
106- total_lines = int (self .textbox .index (" end-1c" ).split ("." )[0 ])
138+ total_lines = int (self .textbox .index (' end-1c' ).split ('.' )[0 ])
107139 line = max (1 , min (line , total_lines ))
108- line_text = self .textbox .get (f" { line } .0" , f" { line } .end" )
140+ line_text = self .textbox .get (f' { line } .0' , f' { line } .end' )
109141 max_col = len (line_text )
110- self .textbox .mark_set (" insert" , f" { line } .0" )
111- self .textbox .see (" insert" )
142+ self .textbox .mark_set (' insert' , f' { line } .0' )
143+ self .textbox .see (' insert' )
112144 self .textbox .focus ()
145+
146+ word_wrap_var = ctk .BooleanVar (value = CONFIGURATION ['word_wrap' ])
147+
148+ def toggle_word_wrap ():
149+ if word_wrap_var .get ():
150+ self .textbox .configure (wrap = 'word' )
151+ CONFIGURATION ['word_wrap' ] = True
152+ else :
153+ self .textbox .configure (wrap = 'none' )
154+ CONFIGURATION ['word_wrap' ] = False
155+
156+ def save_config ():
157+ CONFIGURATION ['font' ] = self .font ._family
158+ CONFIGURATION ['font_size' ] = self .font_size
159+ CONFIGURATION ['word_wrap' ] = word_wrap_var .get ()
160+ CONFIGURATION ['window_geometry' ] = [self .winfo_width (), self .winfo_height ()]
161+ with open (CONFIG_PATH , 'w' ) as f :
162+ json .dump (CONFIGURATION , f , indent = 4 )
113163
114164 menu = Menu (self )
115165 file_menu = Menu (menu , tearoff = 0 )
116- file_menu .add_command (label = " Open" , command = open_from_file )
117- file_menu .add_command (label = " Save As..." , command = save_as )
166+ file_menu .add_command (label = ' Open' , command = open_from_file )
167+ file_menu .add_command (label = ' Save As...' , command = save_as )
118168 file_menu .add_command (label = 'Save...' , command = save_file )
119169 file_menu .add_separator ()
120- file_menu .add_command (label = "Exit" , command = self .destroy )
121- menu .add_cascade (label = "File" , menu = file_menu )
170+ for index , path in CONFIGURATION ['recent_files' ]['recent_files_paths' ]:
171+ file_menu .add_command (label = f'Open { path } ({ index + 1 } )...' )
172+ file_menu .add_separator ()
173+ file_menu .add_command (label = 'Exit' , command = self .destroy )
174+ menu .add_cascade (label = 'File' , menu = file_menu )
122175 font_menu = Menu (menu , tearoff = 0 )
123176 for _font in _fonts :
124177 font_menu .add_command (label = _font , command = lambda f = _font : set_font (f ))
@@ -128,17 +181,20 @@ def go_to_line(event=None):
128181 view_menu .add_command (label = 'Go to End...' , command = go_to_end )
129182 view_menu .add_separator ()
130183 view_menu .add_command (label = 'Go to Line...' , command = go_to_line )
184+ view_menu .add_separator ()
185+ view_menu .add_checkbutton (label = 'Word Wrap...' , onvalue = True , variable = word_wrap_var , command = toggle_word_wrap )
131186 menu .add_cascade (label = 'View' , menu = view_menu )
132187
133188 self .configure (menu = menu )
134189
135- self .font = ctk .CTkFont (family = _fonts [0 ], size = self .font_size )
190+ self .font = ctk .CTkFont (family = CONFIGURATION ['font' ], size = CONFIGURATION ['font_size' ])
191+ self .font_size = CONFIGURATION ['font_size' ]
136192
137193 def update_cursor_info (event = None ):
138- pos = self .textbox .index (" insert" )
139- line , col = map (int , pos .split ("." ))
140- chars = len (self .textbox .get (" 1.0" , " end-1c" ))
141- self .stats_line_col .configure (text = f" Ln: { line } Col: { col + 1 } Ch: { chars } " )
194+ pos = self .textbox .index (' insert' )
195+ line , col = map (int , pos .split ('.' ))
196+ chars = len (self .textbox .get (' 1.0' , ' end-1c' ))
197+ self .stats_line_col .configure (text = f' Ln: { line } Col: { col + 1 } Ch: { chars } ' )
142198 if self .path != None :
143199 self .title (f'kPad - *{ self .path } ' )
144200 else :
@@ -149,16 +205,28 @@ def update_cursor_info(event=None):
149205
150206 self .textbox = ctk .CTkTextbox (self , undo = CONFIGURATION ['undo' ]['enabled' ], autoseparators = CONFIGURATION ['undo' ]['separate_edits_from_undos' ], maxundo = CONFIGURATION ['undo' ]['max_undo' ])
151207 self .textbox .configure (font = self .font )
152-
153- self .textbox .bind ("<KeyRelease>" , update_cursor_info )
154- self .textbox .bind ("<ButtonRelease>" , update_cursor_info )
155- self .bind ("<Control-l>" , go_to_line )
156- self .bind ('<Command-s>' , save_file )
157- self .bind ('<Command-o>' , open_from_file )
158- self .bind ('<Command-t>' , toggle_theme )
159- self .bind ('<Command-n>' , newfile )
160- self .bind ('<Command-plus>' , increment_font_size )
161- self .bind ('<Command-minus>' , decrement_font_size )
208+ if word_wrap_var .get ():
209+ self .textbox .configure (wrap = 'word' )
210+ else :
211+ self .textbox .configure (wrap = 'none' )
212+
213+ self .textbox .bind ('<KeyRelease>' , update_cursor_info )
214+ self .textbox .bind ('<ButtonRelease>' , update_cursor_info )
215+
216+ # Cross-platform key bindings
217+ system = platform .system ()
218+ if system == "Darwin" :
219+ mod = "Command"
220+ else :
221+ mod = "Control"
222+
223+ self .bind (f'<{ mod } -l>' , go_to_line )
224+ self .bind (f'<{ mod } -s>' , save_file )
225+ self .bind (f'<{ mod } -o>' , open_from_file )
226+ self .bind (f'<{ mod } -t>' , toggle_theme )
227+ self .bind (f'<{ mod } -n>' , newfile )
228+ self .bind (f'<{ mod } -equal>' , increment_font_size )
229+ self .bind (f'<{ mod } -minus>' , decrement_font_size )
162230
163231
164232 self .stats_text_frame = ctk .CTkFrame (self )
@@ -168,6 +236,7 @@ def update_cursor_info(event=None):
168236 self .stats_line_col .pack (side = ctk .RIGHT )
169237
170238 self .textbox .pack (fill = 'both' , expand = True )
239+ self .protocol ('WM_DELETE_WINDOW' , lambda : [save_file (), save_config (), self .destroy ()])
171240
172241
173- App ('kPad - Untitled' , [ 500 , 400 ]).mainloop ()
242+ App ('kPad - Untitled' , CONFIGURATION [ 'window_geometry' ]).mainloop ()
0 commit comments