Coverage for C:\leo.repo\leo-editor\leo\core\leoApp.py : 38%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# -*- coding: utf-8 -*-
2#@+leo-ver=5-thin
3#@+node:ekr.20031218072017.2608: * @file leoApp.py
4#@@first
5#@+<< imports >>
6#@+node:ekr.20120219194520.10463: ** << imports >> (leoApp)
7import argparse
8import importlib
9import io
10import os
11import sqlite3
12import subprocess
13import string
14import sys
15import textwrap
16import time
17import traceback
18from typing import Any, Dict
19import zipfile
20import platform
21from leo.core import leoGlobals as g
22from leo.core import leoExternalFiles
23from leo.core.leoQt import QCloseEvent
24StringIO = io.StringIO
25#@-<< imports >>
26#@+others
27#@+node:ekr.20150509193629.1: ** cmd (decorator)
28def cmd(name):
29 """Command decorator for the LeoApp class."""
30 return g.new_cmd_decorator(name, ['g', 'app'])
31#@+node:ekr.20161026122804.1: ** class IdleTimeManager
32class IdleTimeManager:
33 """
34 A singleton class to manage idle-time handling. This class handles all
35 details of running code at idle time, including running 'idle' hooks.
37 Any code can call g.app.idleTimeManager.add_callback(callback) to cause
38 the callback to be called at idle time forever.
39 """
41 def __init__(self):
42 """Ctor for IdleTimeManager class."""
43 self.callback_list = []
44 self.timer = None
45 #@+others
46 #@+node:ekr.20161026125611.1: *3* itm.add_callback
47 def add_callback(self, callback):
48 """Add a callback to be called at every idle time."""
49 self.callback_list.append(callback)
50 #@+node:ekr.20161026124810.1: *3* itm.on_idle
51 on_idle_count = 0
53 def on_idle(self, timer):
54 """IdleTimeManager: Run all idle-time callbacks."""
55 if not g.app:
56 return
57 if g.app.killed:
58 return
59 if not g.app.pluginsController:
60 g.trace('No g.app.pluginsController', g.callers())
61 timer.stop()
62 return # For debugger.
63 self.on_idle_count += 1
64 # Handle the registered callbacks.
65 for callback in self.callback_list:
66 try:
67 callback()
68 except Exception:
69 g.es_exception()
70 g.es_print(f"removing callback: {callback}")
71 self.callback_list.remove(callback)
72 # Handle idle-time hooks.
73 g.app.pluginsController.on_idle()
74 #@+node:ekr.20161028034808.1: *3* itm.start
75 def start(self):
76 """Start the idle-time timer."""
77 self.timer = g.IdleTime(
78 self.on_idle,
79 delay=500,
80 tag='IdleTimeManager.on_idle')
81 if self.timer:
82 self.timer.start()
83 #@-others
84#@+node:ekr.20120209051836.10241: ** class LeoApp
85class LeoApp:
86 """A class representing the Leo application itself.
88 Ivars of this class are Leo's global variables."""
89 #@+others
90 #@+node:ekr.20150509193643.1: *3* app.Birth & startup
91 #@+node:ekr.20031218072017.1416: *4* app.__init__ (helpers contain language dicts)
92 def __init__(self):
93 """
94 Ctor for LeoApp class. These ivars are Leo's global vars.
96 leoGlobals.py contains global switches to be set by hand.
97 """
98 #@+<< LeoApp: command-line arguments >>
99 #@+node:ekr.20161028035755.1: *5* << LeoApp: command-line arguments >>
100 self.batchMode = False # True: run in batch mode.
101 self.debug = [] # A list of switches to be enabled.
102 self.diff = False # True: run Leo in diff mode.
103 self.enablePlugins = True # True: run start1 hook to load plugins. --no-plugins
104 self.failFast = False # True: Use the failfast option in unit tests.
105 self.gui = None # The gui class.
106 self.guiArgName = None # The gui name given in --gui option.
107 self.ipython_inited = False # True if leoIpython.py imports succeeded.
108 self.isTheme = False # True: load files as theme files (ignore myLeoSettings.leo).
109 self.listen_to_log_flag = False # True: execute listen-to-log command.
110 self.loaded_session = False # Set by startup logic to True if no files specified on the command line.
111 self.silentMode = False # True: no signon.
112 self.start_fullscreen = False # For qt_frame plugin.
113 self.start_maximized = False # For qt_frame plugin.
114 self.start_minimized = False # For qt_frame plugin.
115 self.trace_binding = None # The name of a binding to trace, or None.
116 self.trace_setting = None # The name of a setting to trace, or None.
117 self.translateToUpperCase = False # Never set to True.
118 self.useIpython = False # True: add support for IPython.
119 self.use_splash_screen = True # True: put up a splash screen.
120 #@-<< LeoApp: command-line arguments >>
121 #@+<< LeoApp: Debugging & statistics >>
122 #@+node:ekr.20161028035835.1: *5* << LeoApp: Debugging & statistics >>
123 self.debug_dict = {} # For general use.
124 self.disable_redraw = False # True: disable all redraws.
125 self.disableSave = False # May be set by plugins.
126 self.idle_timers = [] # A list of IdleTime instances, so they persist.
127 self.log_listener = None # The process created by the 'listen-for-log' command.
128 self.positions = 0 # The number of positions generated.
129 self.scanErrors = 0 # The number of errors seen by g.scanError.
130 self.structure_errors = 0 # Set by p.safeMoveToThreadNext.
131 self.statsDict = {} # dict used by g.stat, g.clear_stats, g.print_stats.
132 self.statsLockout = False # A lockout to prevent unbound recursion while gathering stats.
133 self.validate_outline = False # True: enables c.validate_outline. (slow)
134 #@-<< LeoApp: Debugging & statistics >>
135 #@+<< LeoApp: error messages >>
136 #@+node:ekr.20161028035902.1: *5* << LeoApp: error messages >>
137 self.menuWarningsGiven = False # True: supress warnings in menu code.
138 self.unicodeErrorGiven = True # True: suppres unicode tracebacks.
139 #@-<< LeoApp: error messages >>
140 #@+<< LeoApp: global directories >>
141 #@+node:ekr.20161028035924.1: *5* << LeoApp: global directories >>
142 self.extensionsDir = None # The leo/extensions directory
143 self.globalConfigDir = None # leo/config directory
144 self.globalOpenDir = None # The directory last used to open a file.
145 self.homeDir = None # The user's home directory.
146 self.homeLeoDir = None # The user's home/.leo directory.
147 self.leoEditorDir = None # The leo-editor directory.
148 self.loadDir = None # The leo/core directory.
149 self.machineDir = None # The machine-specific directory.
150 self.theme_directory = None # The directory from which the theme file was loaded, if any.
151 #@-<< LeoApp: global directories >>
152 #@+<< LeoApp: global data >>
153 #@+node:ekr.20161028035956.1: *5* << LeoApp: global data >>
154 self.atAutoNames = set() # The set of all @auto spellings.
155 self.atFileNames = set() # The set of all built-in @<file> spellings.
156 self.globalKillBuffer = [] # The global kill buffer.
157 self.globalRegisters = {} # The global register list.
158 self.leoID = None # The id part of gnx's.
159 self.loadedThemes = [] # List of loaded theme.leo files.
160 self.lossage = [] # List of last 100 keystrokes.
161 self.paste_c = None # The commander that pasted the last outline.
162 self.spellDict = None # The singleton PyEnchant spell dict.
163 self.numberOfUntitledWindows = 0 # Number of opened untitled windows.
164 self.windowList = [] # Global list of all frames.
165 self.realMenuNameDict = {} # Translations of menu names.
166 #@-<< LeoApp: global data >>
167 #@+<< LeoApp: global controller/manager objects >>
168 #@+node:ekr.20161028040028.1: *5* << LeoApp: global controller/manager objects >>
169 # Most of these are defined in initApp.
170 self.backgroundProcessManager = None
171 # The singleton BackgroundProcessManager instance.
172 self.commander_cacher = None
173 # The singleton leoCacher.CommanderCacher instance.
174 self.commander_db = None
175 # The singleton db, managed by g.app.commander_cacher.
176 self.config = None
177 # The singleton leoConfig instance.
178 self.db = None
179 # The singleton global db, managed by g.app.global_cacher.
180 self.externalFilesController = None
181 # The singleton ExternalFilesController instance.
182 self.global_cacher = None
183 # The singleton leoCacher.GlobalCacher instance.
184 self.idleTimeManager = None
185 # The singleton IdleTimeManager instance.
186 self.ipk = None
187 # python kernel instance
188 self.loadManager = None
189 # The singleton LoadManager instance.
190 # self.logManager = None
191 # The singleton LogManager instance.
192 # self.openWithManager = None
193 # The singleton OpenWithManager instance.
194 self.nodeIndices = None
195 # The singleton nodeIndices instance.
196 self.pluginsController = None
197 # The singleton PluginsManager instance.
198 self.sessionManager = None
199 # The singleton SessionManager instance.
200 # The Commands class...
201 self.commandName = None
202 # The name of the command being executed.
203 self.commandInterruptFlag = False
204 # True: command within a command.
205 #@-<< LeoApp: global controller/manager objects >>
206 #@+<< LeoApp: global reader/writer data >>
207 #@+node:ekr.20170302075110.1: *5* << LeoApp: global reader/writer data >>
208 # From leoAtFile.py.
209 self.atAutoWritersDict = {}
210 self.writersDispatchDict = {}
211 # From leoImport.py
212 self.atAutoDict = {}
213 # Keys are @auto names, values are scanner classes.
214 self.classDispatchDict = {}
215 #@-<< LeoApp: global reader/writer data >>
216 #@+<< LeoApp: global status vars >>
217 #@+node:ekr.20161028040054.1: *5* << LeoApp: global status vars >>
218 self.already_open_files = [] # A list of file names that *might* be open in another copy of Leo.
219 self.dragging = False # True: dragging.
220 self.inBridge = False # True: running from leoBridge module.
221 self.inScript = False # True: executing a script.
222 self.initing = True # True: we are initiing the app.
223 self.initComplete = False # True: late bindings are not allowed.
224 self.initStyleFlag = False # True: setQtStyle called.
225 self.killed = False # True: we are about to destroy the root window.
226 self.openingSettingsFile = False # True, opening a settings file.
227 self.preReadFlag = False # True: we are pre-reading a settings file.
228 self.quitting = False # True: quitting. Locks out some events.
229 self.quit_after_load = False # True: quit immediately after loading. For unit a unit test.
230 self.restarting = False # True: restarting all of Leo. #1240.
231 self.reverting = False # True: executing the revert command.
232 self.syntax_error_files = []
233 #@-<< LeoApp: global status vars >>
234 #@+<< LeoApp: the global log >>
235 #@+node:ekr.20161028040141.1: *5* << LeoApp: the global log >>
236 # To be moved to the LogManager.
237 self.log = None
238 # The LeoFrame containing the present log.
239 self.logInited = False
240 # False: all log message go to logWaiting list.
241 self.logIsLocked = False
242 # True: no changes to log are allowed.
243 self.logWaiting = []
244 # List of tuples (s, color, newline) waiting to go to a log.
245 self.printWaiting = []
246 # Queue of messages to be sent to the printer.
247 self.signon = ''
248 self.signon1 = ''
249 self.signon2 = ''
250 #@-<< LeoApp: the global log >>
251 #@+<< LeoApp: global types >>
252 #@+node:ekr.20161028040204.1: *5* << LeoApp: global types >>
253 from leo.core import leoFrame
254 from leo.core import leoGui
255 self.nullGui = leoGui.NullGui()
256 self.nullLog = leoFrame.NullLog()
257 #@-<< LeoApp: global types >>
258 #@+<< LeoApp: plugins and event handlers >>
259 #@+node:ekr.20161028040229.1: *5* << LeoApp: plugins and event handlers >>
260 self.hookError = False # True: suppress further calls to hooks.
261 self.hookFunction = None # Application wide hook function.
262 self.idle_time_hooks_enabled = True # True: idle-time hooks are enabled.
263 #@-<< LeoApp: plugins and event handlers >>
264 #@+<< LeoApp: scripting ivars >>
265 #@+node:ekr.20161028040303.1: *5* << LeoApp: scripting ivars >>
266 self.scriptDict = {} # For use by scripts. Cleared before running each script.
267 self.scriptResult = None # For use by leoPymacs.
268 self.permanentScriptDict = {} # For use by scripts. Never cleared automatically.
269 #@-<< LeoApp: scripting ivars >>
270 #@+<< LeoApp: unit testing ivars >>
271 #@+node:ekr.20161028040330.1: *5* << LeoApp: unit testing ivars >>
272 self.suppressImportChecks = False # True: suppress importCommands.check
273 #@-<< LeoApp: unit testing ivars >>
274 # Define all global data.
275 self.init_at_auto_names()
276 self.init_at_file_names()
277 self.define_global_constants()
278 self.define_language_delims_dict()
279 self.define_language_extension_dict()
280 self.define_extension_dict()
281 self.define_delegate_language_dict()
282 #@+node:ekr.20141102043816.5: *5* app.define_delegate_language_dict
283 def define_delegate_language_dict(self):
284 self.delegate_language_dict = {
285 # Keys are new language names.
286 # Values are existing languages in leo/modes.
287 "less": "css",
288 "hbs": "html",
289 "handlebars": "html",
290 #"rust": "c",
291 # "vue": "c",
292 }
293 #@+node:ekr.20120522160137.9911: *5* app.define_extension_dict
294 #@@nobeautify
296 def define_extension_dict(self):
298 # Keys are extensions, values are languages
299 self.extension_dict = {
300 # "ada": "ada",
301 "ada": "ada95", # modes/ada95.py exists.
302 "ahk": "autohotkey",
303 "aj": "aspect_j",
304 "apdl": "apdl",
305 "as": "actionscript", # jason 2003-07-03
306 "asp": "asp",
307 "awk": "awk",
308 "b": "b",
309 "bas": "rapidq", # fil 2004-march-11
310 "bash": "shellscript",
311 "bat": "batch",
312 "bbj": "bbj",
313 "bcel": "bcel",
314 "bib": "bibtex",
315 "c": "c",
316 "c++": "cplusplus",
317 "cbl": "cobol", # Only one extension is valid: .cob
318 "cfg": "config",
319 "cfm": "coldfusion",
320 "clj": "clojure", # 2013/09/25: Fix bug 879338.
321 "cljs": "clojure",
322 "cljc": "clojure",
323 "ch": "chill", # Other extensions, .c186,.c286
324 "coffee": "coffeescript",
325 "conf": "apacheconf",
326 "cpp": "cplusplus", # 2020/08/12: was cpp.
327 "css": "css",
328 "d": "d",
329 "dart": "dart",
330 "e": "eiffel",
331 "el": "elisp",
332 "eml": "mail",
333 "erl": "erlang",
334 "ex": "elixir",
335 "f": "fortran",
336 "f90": "fortran90",
337 "factor": "factor",
338 "forth": "forth",
339 "g": "antlr",
340 "go": "go",
341 "groovy": "groovy",
342 "h": "c", # 2012/05/23.
343 "handlebars": "html", # McNab.
344 "hbs": "html", # McNab.
345 "hs": "haskell",
346 "html": "html",
347 "hx": "haxe",
348 "i": "swig",
349 "i4gl": "i4gl",
350 "icn": "icon",
351 "idl": "idl",
352 "inf": "inform",
353 "info": "texinfo",
354 "ini": "ini",
355 "io": "io",
356 "ipynb": "jupyter",
357 "iss": "inno_setup",
358 "java": "java",
359 "jhtml": "jhtml",
360 "jmk": "jmk",
361 "js": "javascript", # For javascript import test.
362 "jsp": "javaserverpage",
363 "json": "json",
364 # "jsp": "jsp",
365 "ksh": "kshell",
366 "kv": "kivy", # PeckJ 2014/05/05
367 "latex": "latex",
368 "less": "css", # McNab
369 "lua": "lua", # ddm 13/02/06
370 "ly": "lilypond",
371 "m": "matlab",
372 "mak": "makefile",
373 "md": "md", # PeckJ 2013/02/07
374 "ml": "ml",
375 "mm": "objective_c", # Only one extension is valid: .m
376 "mod": "modula3",
377 "mpl": "maple",
378 "mqsc": "mqsc",
379 "nqc": "nqc",
380 "nsi": "nsi", # EKR: 2010/10/27
381 # "nsi": "nsis2",
382 "nw": "noweb",
383 "occ": "occam",
384 "otl": "vimoutline", # TL 8/25/08 Vim's outline plugin
385 "p": "pascal",
386 # "p": "pop11", # Conflicts with pascal.
387 "php": "php",
388 "pike": "pike",
389 "pl": "perl",
390 "pl1": "pl1",
391 "po": "gettext",
392 "pod": "perlpod",
393 "pov": "povray",
394 "prg": "foxpro",
395 "pro": "prolog",
396 "ps": "postscript",
397 "psp": "psp",
398 "ptl": "ptl",
399 "py": "python",
400 "pyx": "cython", # Other extensions, .pyd,.pyi
401 # "pyx": "pyrex",
402 # "r": "r", # modes/r.py does not exist.
403 "r": "rebol", # jason 2003-07-03
404 "rb": "ruby", # thyrsus 2008-11-05
405 "rest": "rst",
406 "rex": "objectrexx",
407 "rhtml": "rhtml",
408 "rib": "rib",
409 "rs": "rust", # EKR: 2019/08/11
410 "sas": "sas",
411 "scala": "scala",
412 "scm": "scheme",
413 "scpt": "applescript",
414 "sgml": "sgml",
415 "sh": "shell", # DS 4/1/04. modes/shell.py exists.
416 "shtml": "shtml",
417 "sm": "smalltalk",
418 "splus": "splus",
419 "sql": "plsql", # qt02537 2005-05-27
420 "sqr": "sqr",
421 "ss": "ssharp",
422 "ssi": "shtml",
423 "sty": "latex",
424 "tcl": "tcl", # modes/tcl.py exists.
425 # "tcl": "tcltk",
426 "tex": "latex",
427 # "tex": "tex",
428 "tpl": "tpl",
429 "ts": "typescript",
430 "txt": "plain",
431 # "txt": "text",
432 # "txt": "unknown", # Set when @comment is seen.
433 "uc": "uscript",
434 "v": "verilog",
435 "vbs": "vbscript",
436 "vhd": "vhdl",
437 "vhdl": "vhdl",
438 "vim": "vim",
439 "vtl": "velocity",
440 "w": "cweb",
441 "wiki": "moin",
442 "xml": "xml",
443 "xom": "omnimark",
444 "xsl": "xsl",
445 "yaml": "yaml",
446 "vue": "javascript",
447 "zpt": "zpt",
448 }
450 # These aren't real languages, or have no delims...
451 # cvs_commit, dsssl, embperl, freemarker, hex, jcl,
452 # patch, phpsection, progress, props, pseudoplain,
453 # relax_ng_compact, rtf, svn_commit.
455 # These have extensions which conflict with other languages.
456 # assembly_6502: .asm or .a or .s
457 # assembly_macro32: .asm or .a
458 # assembly_mcs51: .asm or .a
459 # assembly_parrot: .asm or .a
460 # assembly_r2000: .asm or .a
461 # assembly_x86: .asm or .a
462 # squidconf: .conf
463 # rpmspec: .rpm
465 # Extra language extensions, used to associate extensions with mode files.
466 # Used by importCommands.languageForExtension.
467 # Keys are extensions, values are corresponding mode file (without .py)
468 # A value of 'none' is a signal to unit tests that no extension file exists.
469 self.extra_extension_dict = {
470 'pod' : 'perl',
471 'unknown_language': 'none',
472 'w' : 'c',
473 }
474 #@+node:ekr.20031218072017.1417: *5* app.define_global_constants
475 def define_global_constants(self):
476 # self.prolog_string = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
477 self.prolog_prefix_string = "<?xml version=\"1.0\" encoding="
478 self.prolog_postfix_string = "?>"
479 self.prolog_namespace_string = 'xmlns:leo="http://edreamleo.org/namespaces/leo-python-editor/1.1"'
480 #@+node:ekr.20120522160137.9909: *5* app.define_language_delims_dict
481 #@@nobeautify
483 def define_language_delims_dict(self):
485 self.language_delims_dict = {
486 # Internally, lower case is used for all language names.
487 # Keys are languages, values are 1,2 or 3-tuples of delims.
488 "actionscript" : "// /* */", # jason 2003-07-03
489 "ada" : "--",
490 "ada95" : "--",
491 "ahk" : ";",
492 "antlr" : "// /* */",
493 "apacheconf" : "#",
494 "apdl" : "!",
495 "applescript" : "-- (* *)",
496 "asp" : "<!-- -->",
497 "aspect_j" : "// /* */",
498 "assembly_6502" : ";",
499 "assembly_macro32" : ";",
500 "assembly_mcs51" : ";",
501 "assembly_parrot" : "#",
502 "assembly_r2000" : "#",
503 "assembly_x86" : ";",
504 "autohotkey" : "; /* */", # TL - AutoHotkey language
505 "awk" : "#",
506 "b" : "// /* */",
507 "batch" : "REM_", # Use the REM hack.
508 "bbj" : "/* */",
509 "bcel" : "// /* */",
510 "bibtex" : "%",
511 "c" : "// /* */", # C, C++ or objective C.
512 "chill" : "/* */",
513 "clojure" : ";", # 2013/09/25: Fix bug 879338.
514 "cobol" : "*",
515 "coldfusion" : "<!-- -->",
516 "coffeescript" : "#", # 2016/02/26.
517 "config" : "#", # Leo 4.5.1
518 "cplusplus" : "// /* */",
519 "cpp" : "// /* */",# C++.
520 "csharp" : "// /* */", # C#
521 "css" : "/* */", # 4/1/04
522 "cweb" : "@q@ @>", # Use the "cweb hack"
523 "cython" : "#",
524 "d" : "// /* */",
525 "dart" : "// /* */", # Leo 5.0.
526 "doxygen" : "#",
527 "eiffel" : "--",
528 "elisp" : ";",
529 "erlang" : "%",
530 "elixir" : "#",
531 "factor" : "!_ ( )", # Use the rem hack.
532 "forth" : "\\_ _(_ _)", # Use the "REM hack"
533 "fortran" : "C",
534 "fortran90" : "!",
535 "foxpro" : "&&",
536 "gettext" : "# ",
537 "go" : "//",
538 "groovy" : "// /* */",
539 "handlebars" : "<!-- -->", # McNab: delegate to html.
540 "haskell" : "--_ {-_ _-}",
541 "haxe" : "// /* */",
542 "hbs" : "<!-- -->", # McNab: delegate to html.
543 "html" : "<!-- -->",
544 "i4gl" : "-- { }",
545 "icon" : "#",
546 "idl" : "// /* */",
547 "inform" : "!",
548 "ini" : ";",
549 "inno_setup" : ";",
550 "interlis" : "/* */",
551 "io" : "// */",
552 "java" : "// /* */",
553 "javascript" : "// /* */", # EKR: 2011/11/12: For javascript import test.
554 "javaserverpage" : "<%-- --%>", # EKR: 2011/11/25 (See also, jsp)
555 "jhtml" : "<!-- -->",
556 "jmk" : "#",
557 "json" : "#", # EKR: 2020/07/27: Json has no delims. This is a dummy entry.
558 "jsp" : "<%-- --%>",
559 "jupyter" : "<%-- --%>", # Default to markdown?
560 "kivy" : "#", # PeckJ 2014/05/05
561 "kshell" : "#", # Leo 4.5.1.
562 "latex" : "%",
563 "less" : "/* */", # NcNab: delegate to css.
564 "lilypond" : "% %{ %}",
565 "lisp" : ";", # EKR: 2010/09/29
566 "lotos" : "(* *)",
567 "lua" : "--", # ddm 13/02/06
568 "mail" : ">",
569 "makefile" : "#",
570 "maple" : "//",
571 "markdown" : "<!-- -->", # EKR, 2018/03/03: html comments.
572 "matlab" : "%", # EKR: 2011/10/21
573 "md" : "<!-- -->", # PeckJ: 2013/02/08
574 "ml" : "(* *)",
575 "modula3" : "(* *)",
576 "moin" : "##",
577 "mqsc" : "*",
578 "netrexx" : "-- /* */",
579 "noweb" : "%", # EKR: 2009-01-30. Use Latex for doc chunks.
580 "nqc" : "// /* */",
581 "nsi" : ";", # EKR: 2010/10/27
582 "nsis2" : ";",
583 "objective_c" : "// /* */",
584 "objectrexx" : "-- /* */",
585 "occam" : "--",
586 "omnimark" : ";",
587 "pandoc" : "<!-- -->",
588 "pascal" : "// { }",
589 "perl" : "#",
590 "perlpod" : "# __=pod__ __=cut__", # 9/25/02: The perlpod hack.
591 "php" : "// /* */", # 6/23/07: was "//",
592 "pike" : "// /* */",
593 "pl1" : "/* */",
594 "plain" : "#", # We must pick something.
595 "plsql" : "-- /* */", # SQL scripts qt02537 2005-05-27
596 "pop11" : ";;; /* */",
597 "postscript" : "%",
598 "povray" : "// /* */",
599 "powerdynamo" : "// <!-- -->",
600 "prolog" : "% /* */",
601 "psp" : "<!-- -->",
602 "ptl" : "#",
603 "pvwave" : ";",
604 "pyrex" : "#",
605 "python" : "#",
606 "r" : "#",
607 "rapidq" : "'", # fil 2004-march-11
608 "rebol" : ";", # jason 2003-07-03
609 "redcode" : ";",
610 "rest" : ".._",
611 "rhtml" : "<%# %>",
612 "rib" : "#",
613 "rpmspec" : "#",
614 "rst" : ".._",
615 "rust" : "// /* */",
616 "ruby" : "#", # thyrsus 2008-11-05
617 "rview" : "// /* */",
618 "sas" : "* /* */",
619 "scala" : "// /* */",
620 "scheme" : "; #| |#",
621 "sdl_pr" : "/* */",
622 "sgml" : "<!-- -->",
623 "shell" : "#", # shell scripts
624 "shellscript" : "#",
625 "shtml" : "<!-- -->",
626 "smalltalk" : '" "', # Comments are enclosed in double quotes(!!)
627 "smi_mib" : "--",
628 "splus" : "#",
629 "sqr" : "!",
630 "squidconf" : "#",
631 "ssharp" : "#",
632 "swig" : "// /* */",
633 "tcl" : "#",
634 "tcltk" : "#",
635 "tex" : "%", # Bug fix: 2008-1-30: Fixed Mark Edginton's bug.
636 "text" : "#", # We must pick something.
637 "texinfo" : "@c",
638 "tpl" : "<!-- -->",
639 "tsql" : "-- /* */",
640 "typescript" : "// /* */", # For typescript import test.
641 "unknown" : "#", # Set when @comment is seen.
642 "unknown_language" : '#--unknown-language--', # For unknown extensions in @shadow files.
643 "uscript" : "// /* */",
644 "vbscript" : "'",
645 "velocity" : "## #* *#",
646 "verilog" : "// /* */",
647 "vhdl" : "--",
648 "vim" : "\"",
649 "vimoutline" : "#", # TL 8/25/08 Vim's outline plugin
650 "xml" : "<!-- -->",
651 "xsl" : "<!-- -->",
652 "xslt" : "<!-- -->",
653 "yaml" : "#",
654 "zpt" : "<!-- -->",
656 # These aren't real languages, or have no delims...
657 # "cvs_commit" : "",
658 # "dsssl" : "; <!-- -->",
659 # "embperl" : "<!-- -->", # Internal colorizing state.
660 # "freemarker" : "",
661 # "hex" : "",
662 # "jcl" : "",
663 # "patch" : "",
664 # "phpsection" : "<!-- -->", # Internal colorizing state.
665 # "props" : "#", # Unknown language.
666 # "pseudoplain" : "",
667 # "relax_ng_compact" : "#", # An xml schema.
668 # "rtf" : "",
669 # "svn_commit" : "",
670 }
671 #@+node:ekr.20120522160137.9910: *5* app.define_language_extension_dict
672 #@@nobeautify
674 def define_language_extension_dict(self):
676 # Used only by g.app.externalFilesController.get_ext.
678 # Keys are languages, values are extensions.
679 self.language_extension_dict = {
680 "actionscript" : "as", # jason 2003-07-03
681 "ada" : "ada",
682 "ada95" : "ada",
683 "ahk" : "ahk",
684 "antlr" : "g",
685 "apacheconf" : "conf",
686 "apdl" : "apdl",
687 "applescript" : "scpt",
688 "asp" : "asp",
689 "aspect_j" : "aj",
690 "autohotkey" : "ahk", # TL - AutoHotkey language
691 "awk" : "awk",
692 "b" : "b",
693 "batch" : "bat", # Leo 4.5.1.
694 "bbj" : "bbj",
695 "bcel" : "bcel",
696 "bibtex" : "bib",
697 "c" : "c",
698 "chill" : "ch", # Only one extension is valid: .c186, .c286
699 "clojure" : "clj", # 2013/09/25: Fix bug 879338.
700 "cobol" : "cbl", # Only one extension is valid: .cob
701 "coldfusion" : "cfm",
702 "coffeescript" : "coffee",
703 "config" : "cfg",
704 "cplusplus" : "c++",
705 "cpp" : "cpp",
706 "css" : "css", # 4/1/04
707 "cweb" : "w",
708 "cython" : "pyx", # Only one extension is valid at present: .pyi, .pyd.
709 "d" : "d",
710 "dart" : "dart",
711 "eiffel" : "e",
712 "elisp" : "el",
713 "erlang" : "erl",
714 "elixir" : "ex",
715 "factor" : "factor",
716 "forth" : "forth",
717 "fortran" : "f",
718 "fortran90" : "f90",
719 "foxpro" : "prg",
720 "gettext" : "po",
721 "go" : "go",
722 "groovy" : "groovy",
723 "haskell" : "hs",
724 "haxe" : "hx",
725 "html" : "html",
726 "i4gl" : "i4gl",
727 "icon" : "icn",
728 "idl" : "idl",
729 "inform" : "inf",
730 "ini" : "ini",
731 "inno_setup" : "iss",
732 "io" : "io",
733 "java" : "java",
734 "javascript" : "js", # EKR: 2011/11/12: For javascript import test.
735 "javaserverpage": "jsp", # EKR: 2011/11/25
736 "jhtml" : "jhtml",
737 "jmk" : "jmk",
738 "json" : "json",
739 "jsp" : "jsp",
740 "jupyter" : "ipynb",
741 "kivy" : "kv", # PeckJ 2014/05/05
742 "kshell" : "ksh", # Leo 4.5.1.
743 "latex" : "tex", # 1/8/04
744 "lilypond" : "ly",
745 "lua" : "lua", # ddm 13/02/06
746 "mail" : "eml",
747 "makefile" : "mak",
748 "maple" : "mpl",
749 "matlab" : "m",
750 "md" : "md", # PeckJ: 2013/02/07
751 "ml" : "ml",
752 "modula3" : "mod",
753 "moin" : "wiki",
754 "mqsc" : "mqsc",
755 "noweb" : "nw",
756 "nqc" : "nqc",
757 "nsi" : "nsi", # EKR: 2010/10/27
758 "nsis2" : "nsi",
759 "objective_c" : "mm", # Only one extension is valid: .m
760 "objectrexx" : "rex",
761 "occam" : "occ",
762 "omnimark" : "xom",
763 "pascal" : "p",
764 "perl" : "pl",
765 "perlpod" : "pod",
766 "php" : "php",
767 "pike" : "pike",
768 "pl1" : "pl1",
769 "plain" : "txt",
770 "plsql" : "sql", # qt02537 2005-05-27
771 # "pop11" : "p", # Conflicts with pascall.
772 "postscript" : "ps",
773 "povray" : "pov",
774 "prolog" : "pro",
775 "psp" : "psp",
776 "ptl" : "ptl",
777 "pyrex" : "pyx",
778 "python" : "py",
779 "r" : "r",
780 "rapidq" : "bas", # fil 2004-march-11
781 "rebol" : "r", # jason 2003-07-03
782 "rhtml" : "rhtml",
783 "rib" : "rib",
784 "rst" : "rest",
785 "ruby" : "rb", # thyrsus 2008-11-05
786 "rust" : "rs", # EKR: 2019/08/11
787 "sas" : "sas",
788 "scala" : "scala",
789 "scheme" : "scm",
790 "sgml" : "sgml",
791 "shell" : "sh", # DS 4/1/04
792 "shellscript" : "bash",
793 "shtml" : "ssi", # Only one extension is valid: .shtml
794 "smalltalk" : "sm",
795 "splus" : "splus",
796 "sqr" : "sqr",
797 "ssharp" : "ss",
798 "swig" : "i",
799 "tcl" : "tcl",
800 "tcltk" : "tcl",
801 "tex" : "tex",
802 "texinfo" : "info",
803 "text" : "txt",
804 "tpl" : "tpl",
805 "tsql" : "sql", # A guess.
806 "typescript" : "ts",
807 "unknown" : "txt", # Set when @comment is seen.
808 "uscript" : "uc",
809 "vbscript" : "vbs",
810 "velocity" : "vtl",
811 "verilog" : "v",
812 "vhdl" : "vhd", # Only one extension is valid: .vhdl
813 "vim" : "vim",
814 "vimoutline" : "otl", # TL 8/25/08 Vim's outline plugin
815 "xml" : "xml",
816 "xsl" : "xsl",
817 "xslt" : "xsl",
818 "yaml" : "yaml",
819 "zpt" : "zpt",
820 }
822 # These aren't real languages, or have no delims...
823 # cvs_commit, dsssl, embperl, freemarker, hex, jcl,
824 # patch, phpsection, progress, props, pseudoplain,
825 # relax_ng_compact, rtf, svn_commit.
827 # These have extensions which conflict with other languages.
828 # assembly_6502: .asm or .a or .s
829 # assembly_macro32: .asm or .a
830 # assembly_mcs51: .asm or .a
831 # assembly_parrot: .asm or .a
832 # assembly_r2000: .asm or .a
833 # assembly_x86: .asm or .a
834 # squidconf: .conf
835 # rpmspec: .rpm
836 #@+node:ekr.20140729162415.18086: *5* app.init_at_auto_names
837 def init_at_auto_names(self):
838 """Init the app.atAutoNames set."""
839 self.atAutoNames = set([
840 "@auto-rst", "@auto",
841 ])
842 #@+node:ekr.20140729162415.18091: *5* app.init_at_file_names
843 def init_at_file_names(self):
844 """Init the app.atFileNames set."""
845 self.atFileNames = set([
846 "@asis",
847 "@edit",
848 "@file-asis", "@file-thin", "@file-nosent", "@file",
849 "@clean", "@nosent",
850 "@shadow",
851 "@thin",
852 ])
853 #@+node:ekr.20090717112235.6007: *4* app.computeSignon & printSignon
854 def computeSignon(self):
855 from leo.core import leoVersion
856 app = self
857 guiVersion = ', ' + app.gui.getFullVersion() if app.gui else ''
858 leoVer = leoVersion.version
859 n1, n2, n3, junk1, junk2 = sys.version_info
860 if sys.platform.startswith('win'):
861 sysVersion = 'Windows '
862 try:
863 # peckj 20140416: determine true OS architecture
864 # the following code should return the proper architecture
865 # regardless of whether or not the python architecture matches
866 # the OS architecture (i.e. python 32-bit on windows 64-bit will return 64-bit)
867 v = platform.win32_ver()
868 release, winbuild, sp, ptype = v
869 true_platform = os.environ['PROCESSOR_ARCHITECTURE']
870 try:
871 true_platform = os.environ['PROCESSOR_ARCHITEw6432']
872 except KeyError:
873 pass
874 sysVersion = f"Windows {release} {true_platform} (build {winbuild}) {sp}"
875 except Exception:
876 pass
877 else: sysVersion = sys.platform
878 branch, junk_commit = g.gitInfo()
879 author, commit, date = g.getGitVersion()
880 # Compute g.app.signon.
881 signon = [f"Leo {leoVer}"]
882 if branch:
883 signon.append(f", {branch} branch")
884 if commit:
885 signon.append(', build ' + commit)
886 if date:
887 signon.append('\n' + date)
888 app.signon = ''.join(signon)
889 # Compute g.app.signon1.
890 app.signon1 = f"Python {n1}.{n2}.{n3}{guiVersion}\n{sysVersion}"
892 def printSignon(self, verbose=False):
893 """Print a minimal sigon to the log."""
894 app = self
895 if app.silentMode:
896 return
897 if sys.stdout.encoding and sys.stdout.encoding.lower() != 'utf-8':
898 print('Note: sys.stdout.encoding is not UTF-8')
899 print(f"Encoding is: {sys.stdout.encoding!r}")
900 print('See: https://stackoverflow.com/questions/14109024')
901 print('')
902 print(app.signon)
903 if verbose:
904 print(app.signon1)
905 #@+node:ekr.20100831090251.5838: *4* app.createXGui
906 #@+node:ekr.20100831090251.5840: *5* app.createCursesGui
907 def createCursesGui(self, fileName='', verbose=False):
908 try:
909 import curses
910 assert curses
911 except Exception:
912 # g.es_exception()
913 print('can not import curses.')
914 if g.isWindows:
915 print('Windows: pip install windows-curses')
916 sys.exit()
917 try:
918 from leo.plugins import cursesGui2
919 ok = cursesGui2.init()
920 if ok:
921 g.app.gui = cursesGui2.LeoCursesGui()
922 except Exception:
923 g.es_exception()
924 print('can not create curses gui.')
925 sys.exit()
926 #@+node:ekr.20181031160401.1: *5* app.createBrowserGui
927 def createBrowserGui(self, fileName='', verbose=False):
928 app = self
929 try:
930 from flexx import flx
931 assert flx
932 except Exception:
933 g.es_exception()
934 print('can not import flexx')
935 sys.exit(1)
936 try:
937 from leo.plugins import leoflexx
938 assert leoflexx
939 except Exception:
940 g.es_exception()
941 print('can not import leo.plugins.leoflexx')
942 sys.exit(1)
943 g.app.gui = leoflexx.LeoBrowserGui(gui_name=app.guiArgName)
944 #@+node:ekr.20090619065122.8593: *5* app.createDefaultGui
945 def createDefaultGui(self, fileName='', verbose=False):
946 """A convenience routines for plugins to create the default gui class."""
947 app = self
948 argName = app.guiArgName
949 if g.in_bridge:
950 return # The bridge will create the gui later.
951 if app.gui:
952 return # This method can be called twice if we had to get .leoID.txt.
953 if argName == 'qt':
954 app.createQtGui(fileName, verbose=verbose)
955 elif argName == 'null':
956 g.app.gui = g.app.nullGui
957 elif argName.startswith('browser'):
958 app.createBrowserGui()
959 elif argName in ('console', 'curses'):
960 app.createCursesGui()
961 elif argName == 'text':
962 app.createTextGui()
963 if not app.gui:
964 print('createDefaultGui: Leo requires Qt to be installed.')
965 #@+node:ekr.20031218072017.1938: *5* app.createNullGuiWithScript
966 def createNullGuiWithScript(self, script=None):
967 app = self
968 app.batchMode = True
969 app.gui = g.app.nullGui
970 app.gui.setScript(script)
971 #@+node:ekr.20090202191501.1: *5* app.createQtGui
972 def createQtGui(self, fileName='', verbose=False):
973 # Do NOT omit fileName param: it is used in plugin code.
974 """A convenience routines for plugins to create the Qt gui class."""
975 app = self
976 try:
977 from leo.core.leoQt import Qt
978 assert Qt
979 except Exception:
980 # #1215: Raise an emergency dialog.
981 message = 'Can not Import Qt'
982 print(message)
983 try:
984 d = g.EmergencyDialog(title=message, message=message)
985 d.run()
986 except Exception:
987 g.es_exception()
988 sys.exit(1)
989 try:
990 from leo.plugins import qt_gui
991 except Exception:
992 g.es_exception()
993 print('can not import leo.plugins.qt_gui')
994 sys.exit(1)
995 try:
996 from leo.plugins.editpane.editpane import edit_pane_test_open, edit_pane_csv
997 g.command('edit-pane-test-open')(edit_pane_test_open)
998 g.command('edit-pane-csv')(edit_pane_csv)
999 except ImportError:
1000 # g.es_exception()
1001 print('Failed to import editpane')
1002 #
1003 # Complete the initialization.
1004 qt_gui.init()
1005 if app.gui and fileName and verbose:
1006 print(f"Qt Gui created in {fileName}")
1007 #@+node:ekr.20170419093747.1: *5* app.createTextGui (was createCursesGui)
1008 def createTextGui(self, fileName='', verbose=False):
1009 app = self
1010 app.pluginsController.loadOnePlugin('leo.plugins.cursesGui', verbose=verbose)
1011 #@+node:ekr.20090126063121.3: *5* app.createWxGui
1012 def createWxGui(self, fileName='', verbose=False):
1013 # Do NOT omit fileName param: it is used in plugin code.
1014 """A convenience routines for plugins to create the wx gui class."""
1015 app = self
1016 app.pluginsController.loadOnePlugin('leo.plugins.wxGui', verbose=verbose)
1017 if fileName and verbose:
1018 print(f"wxGui created in {fileName}")
1019 #@+node:ville.20090620122043.6275: *4* app.setGlobalDb
1020 def setGlobalDb(self):
1021 """ Create global pickleshare db
1023 Usable by::
1025 g.app.db['hello'] = [1,2,5]
1027 """
1028 # Fixes bug 670108.
1029 from leo.core import leoCache
1030 g.app.global_cacher = leoCache.GlobalCacher()
1031 g.app.db = g.app.global_cacher.db
1032 g.app.commander_cacher = leoCache.CommanderCacher()
1033 g.app.commander_db = g.app.commander_cacher.db
1034 #@+node:ekr.20031218072017.1978: *4* app.setLeoID & helpers
1035 def setLeoID(self, useDialog=True, verbose=True):
1036 """Get g.app.leoID from various sources."""
1037 self.leoID = None
1038 assert self == g.app
1039 verbose = verbose and not g.unitTesting and not self.silentMode
1040 table = (self.setIDFromSys, self.setIDFromFile, self.setIDFromEnv,)
1041 for func in table:
1042 func(verbose)
1043 if self.leoID:
1044 return self.leoID
1045 if useDialog:
1046 self.setIdFromDialog()
1047 if self.leoID:
1048 self.setIDFile()
1049 return self.leoID
1050 #@+node:ekr.20191017061451.1: *5* app.cleanLeoID
1051 def cleanLeoID(self, id_, tag):
1052 """#1404: Make sure that the given Leo ID will not corrupt a .leo file."""
1053 old_id = id_ if isinstance(id_, str) else repr(id_)
1054 try:
1055 id_ = id_.replace('.', '').replace(',', '').replace('"', '').replace("'", '')
1056 # Remove *all* whitespace: https://stackoverflow.com/questions/3739909
1057 id_ = ''.join(id_.split())
1058 except Exception:
1059 g.es_exception()
1060 id_ = ''
1061 if len(id_) < 3:
1062 g.EmergencyDialog(
1063 title=f"Invalid Leo ID: {tag}",
1064 message=(
1065 f"Invalid Leo ID: {old_id!r}\n\n"
1066 "Your id should contain only letters and numbers\n"
1067 "and must be at least 3 characters in length."))
1068 return id_
1069 #@+node:ekr.20031218072017.1979: *5* app.setIDFromSys
1070 def setIDFromSys(self, verbose):
1071 """
1072 Attempt to set g.app.leoID from sys.leoID.
1074 This might be set by in Python's sitecustomize.py file.
1075 """
1076 id_ = getattr(sys, "leoID", None)
1077 if id_:
1078 # Careful: periods in the id field of a gnx will corrupt the .leo file!
1079 id_ = self.cleanLeoID(id_, 'sys.leoID')
1080 # cleanLeoID raises a warning dialog.
1081 if len(id_) > 2:
1082 self.leoID = id_
1083 if verbose:
1084 g.red("leoID=", self.leoID, spaces=False)
1085 #@+node:ekr.20031218072017.1980: *5* app.setIDFromFile
1086 def setIDFromFile(self, verbose):
1087 """Attempt to set g.app.leoID from leoID.txt."""
1088 tag = ".leoID.txt"
1089 for theDir in (self.homeLeoDir, self.globalConfigDir, self.loadDir):
1090 if not theDir:
1091 continue # Do not use the current directory!
1092 fn = g.os_path_join(theDir, tag)
1093 try:
1094 with open(fn, 'r') as f:
1095 s = f.readline().strip()
1096 if not s:
1097 continue
1098 # #1404: Ensure valid ID.
1099 id_ = self.cleanLeoID(s, tag)
1100 # cleanLeoID raises a warning dialog.
1101 if len(id_) > 2:
1102 self.leoID = id_
1103 return
1104 except IOError:
1105 pass
1106 except Exception:
1107 g.error('unexpected exception in app.setLeoID')
1108 g.es_exception()
1109 #@+node:ekr.20060211140947.1: *5* app.setIDFromEnv
1110 def setIDFromEnv(self, verbose):
1111 """Set leoID from environment vars."""
1112 try:
1113 id_ = os.getenv('USER')
1114 if id_:
1115 if verbose:
1116 g.blue("setting leoID from os.getenv('USER'):", repr(id_))
1117 # Careful: periods in the gnx would corrupt the .leo file!
1118 id_ = self.cleanLeoID(id_, "os.getenv('USER')")
1119 # cleanLeoID raises a warning dialog.
1120 if len(id_) > 2:
1121 self.leoID = id_
1122 except Exception:
1123 pass
1124 #@+node:ekr.20031218072017.1981: *5* app.setIdFromDialog
1125 def setIdFromDialog(self):
1126 """Get leoID from a Tk dialog."""
1127 #
1128 # Don't put up a splash screen: it would obscure the coming dialog.
1129 self.use_splash_screen = False
1130 #
1131 # Get the id, making sure it is at least three characters long.
1132 while True:
1133 dialog = g.TkIDDialog()
1134 dialog.run()
1135 # #1404: Make sure the id will not corrupt the .leo file.
1136 id_ = self.cleanLeoID(dialog.val, "")
1137 if id_ and len(id_) > 2:
1138 break
1139 #
1140 # Put result in g.app.leoID.
1141 self.leoID = id_
1142 g.blue('leoID=', repr(self.leoID), spaces=False)
1143 #@+node:ekr.20031218072017.1982: *5* app.setIDFile
1144 def setIDFile(self):
1145 """Create leoID.txt."""
1146 tag = ".leoID.txt"
1147 for theDir in (self.homeLeoDir, self.globalConfigDir, self.loadDir):
1148 if theDir:
1149 try:
1150 fn = g.os_path_join(theDir, tag)
1151 with open(fn, 'w') as f:
1152 f.write(self.leoID)
1153 if g.os_path_exists(fn):
1154 g.error('', tag, 'created in', theDir)
1155 return
1156 except IOError:
1157 pass
1158 g.error('can not create', tag, 'in', theDir)
1159 #@+node:ekr.20031218072017.1847: *4* app.setLog, lockLog, unlocklog
1160 def setLog(self, log):
1161 """set the frame to which log messages will go"""
1162 if not self.logIsLocked:
1163 self.log = log
1165 def lockLog(self):
1166 """Disable changes to the log"""
1167 # print("app.lockLog:")
1168 self.logIsLocked = True
1170 def unlockLog(self):
1171 """Enable changes to the log"""
1172 # print("app.unlockLog:")
1173 self.logIsLocked = False
1174 #@+node:ekr.20031218072017.2619: *4* app.writeWaitingLog
1175 def writeWaitingLog(self, c):
1176 """Write all waiting lines to the log."""
1177 #
1178 # Do not call g.es, g.es_print, g.pr or g.trace here!
1179 app = self
1180 if not c or not c.exists:
1181 return
1182 if g.unitTesting:
1183 app.logWaiting = []
1184 g.app.setLog(None) # Prepare to requeue for other commanders.
1185 return
1186 # Write the signon to the log: similar to self.computeSignon().
1187 table = [
1188 ('Leo Log Window', 'red'),
1189 (app.signon, None),
1190 (app.signon1, None),
1191 ]
1192 table.reverse()
1193 c.setLog()
1194 app.logInited = True # Prevent recursive call.
1195 if not app.silentMode:
1196 # Write the signon.
1197 for s, color in table:
1198 if s:
1199 app.logWaiting.insert(0, (s, color, True),)
1200 # Write all the queued log entries.
1201 for msg in app.logWaiting:
1202 s, color, newline = msg[:3]
1203 kwargs = {} if len(msg) < 4 else msg[3]
1204 kwargs = {
1205 k: v for k, v in kwargs.items() if k not in ('color', 'newline')}
1206 g.es('', s, color=color, newline=newline, **kwargs)
1207 if hasattr(c.frame.log, 'scrollToEnd'):
1208 g.app.gui.runAtIdle(c.frame.log.scrollToEnd)
1209 app.logWaiting = []
1210 # Essential when opening multiple files...
1211 g.app.setLog(None)
1212 #@+node:ekr.20180924093227.1: *3* app.c property
1213 @property
1214 def c(self):
1215 return self.log and self.log.c
1216 #@+node:ekr.20171127111053.1: *3* app.Closing
1217 #@+node:ekr.20031218072017.2609: *4* app.closeLeoWindow
1218 def closeLeoWindow(self, frame, new_c=None, finish_quit=True):
1219 """
1220 Attempt to close a Leo window.
1222 Return False if the user veto's the close.
1224 finish_quit - usually True, close Leo when last file closes, but
1225 False when closing an already-open-elsewhere file
1226 during initial load, so UI remains for files
1227 further along the command line.
1228 """
1229 c = frame.c
1230 if 'shutdown' in g.app.debug:
1231 g.trace(f"changed: {c.changed} {c.shortFileName()}")
1232 c.endEditing() # Commit any open edits.
1233 if c.promptingForClose:
1234 # There is already a dialog open asking what to do.
1235 return False
1236 g.app.recentFilesManager.writeRecentFilesFile(c)
1237 # Make sure .leoRecentFiles.txt is written.
1238 if c.changed:
1239 c.promptingForClose = True
1240 veto = frame.promptForSave()
1241 c.promptingForClose = False
1242 if veto:
1243 return False
1244 g.app.setLog(None) # no log until we reactive a window.
1245 g.doHook("close-frame", c=c)
1246 #
1247 # Save the window state for *all* open files.
1248 g.app.commander_cacher.commit()
1249 # store cache, but don't close it.
1250 # This may remove frame from the window list.
1251 if frame in g.app.windowList:
1252 g.app.destroyWindow(frame)
1253 g.app.windowList.remove(frame)
1254 else:
1255 # #69.
1256 g.app.forgetOpenFile(fn=c.fileName())
1257 if g.app.windowList:
1258 c2 = new_c or g.app.windowList[0].c
1259 g.app.selectLeoWindow(c2)
1260 elif finish_quit and not g.unitTesting:
1261 g.app.finishQuit()
1262 return True # The window has been closed.
1263 #@+node:ekr.20031218072017.2612: *4* app.destroyAllOpenWithFiles
1264 def destroyAllOpenWithFiles(self):
1265 """Remove temp files created with the Open With command."""
1266 if 'shutdown' in g.app.debug:
1267 g.pr('destroyAllOpenWithFiles')
1268 if g.app.externalFilesController:
1269 g.app.externalFilesController.shut_down()
1270 g.app.externalFilesController = None
1271 #@+node:ekr.20031218072017.2615: *4* app.destroyWindow
1272 def destroyWindow(self, frame):
1273 """Destroy all ivars in a Leo frame."""
1274 if 'shutdown' in g.app.debug:
1275 g.pr(f"destroyWindow: {frame.c.shortFileName()}")
1276 if g.app.externalFilesController:
1277 g.app.externalFilesController.destroy_frame(frame)
1278 if frame in g.app.windowList:
1279 g.app.forgetOpenFile(frame.c.fileName())
1280 # force the window to go away now.
1281 # Important: this also destroys all the objects of the commander.
1282 frame.destroySelf()
1283 #@+node:ekr.20031218072017.1732: *4* app.finishQuit
1284 def finishQuit(self):
1285 # forceShutdown may already have fired the "end1" hook.
1286 assert self == g.app, repr(g.app)
1287 trace = 'shutdown' in g.app.debug
1288 if trace:
1289 g.pr('finishQuit: killed:', g.app.killed)
1290 if not g.app.killed:
1291 g.doHook("end1")
1292 if g.app.global_cacher: # #1766.
1293 g.app.global_cacher.commit_and_close()
1294 if g.app.commander_cacher: # #1766.
1295 g.app.commander_cacher.commit()
1296 g.app.commander_cacher.close()
1297 if g.app.ipk:
1298 g.app.ipk.cleanup_consoles()
1299 g.app.destroyAllOpenWithFiles()
1300 if hasattr(g.app, 'pyzo_close_handler'):
1301 # pylint: disable=no-member
1302 g.app.pyzo_close_handler()
1303 g.app.killed = True
1304 # Disable all further hooks and events.
1305 # Alas, "idle" events can still be called
1306 # even after the following code.
1307 if g.app.gui:
1308 g.app.gui.destroySelf()
1309 # Calls qtApp.quit()
1310 #@+node:ekr.20031218072017.2616: *4* app.forceShutdown
1311 def forceShutdown(self):
1312 """
1313 Forces an immediate shutdown of Leo at any time.
1315 In particular, may be called from plugins during startup.
1316 """
1317 trace = 'shutdown' in g.app.debug
1318 app = self
1319 if trace:
1320 g.pr('forceShutdown')
1321 for c in app.commanders():
1322 app.forgetOpenFile(c.fileName())
1323 # Wait until everything is quiet before really quitting.
1324 if trace:
1325 g.pr('forceShutdown: before end1')
1326 g.doHook("end1")
1327 if trace:
1328 g.pr('forceShutdown: after end1')
1329 self.log = None # Disable writeWaitingLog
1330 self.killed = True # Disable all further hooks.
1331 for w in self.windowList[:]:
1332 if trace:
1333 g.pr(f"forceShutdown: {w}")
1334 self.destroyWindow(w)
1335 if trace:
1336 g.pr('before finishQuit')
1337 self.finishQuit()
1338 #@+node:ekr.20031218072017.2617: *4* app.onQuit
1339 @cmd('exit-leo')
1340 @cmd('quit-leo')
1341 def onQuit(self, event=None):
1342 """Exit Leo, prompting to save unsaved outlines first."""
1343 if 'shutdown' in g.app.debug:
1344 g.trace()
1345 # #2433 - use the same method as clicking on the close box.
1346 g.app.gui.close_event(QCloseEvent()) # type:ignore
1347 #@+node:ville.20090602181814.6219: *3* app.commanders
1348 def commanders(self):
1349 """ Return list of currently active controllers """
1350 return [f.c for f in g.app.windowList]
1351 #@+node:ekr.20120427064024.10068: *3* app.Detecting already-open files
1352 #@+node:ekr.20120427064024.10064: *4* app.checkForOpenFile
1353 def checkForOpenFile(self, c, fn):
1354 """Warn if fn is already open and add fn to already_open_files list."""
1355 d, tag = g.app.db, 'open-leo-files'
1356 if g.app.reverting:
1357 # #302: revert to saved doesn't reset external file change monitoring
1358 g.app.already_open_files = []
1359 if (d is None or
1360 g.unitTesting or
1361 g.app.batchMode or
1362 g.app.reverting or
1363 g.app.inBridge
1364 ):
1365 return
1366 # #1519: check os.path.exists.
1367 aList = g.app.db.get(tag) or [] # A list of normalized file names.
1368 if any(os.path.exists(z) and os.path.samefile(z, fn) for z in aList):
1369 # The file may be open in another copy of Leo, or not:
1370 # another Leo may have been killed prematurely.
1371 # Put the file on the global list.
1372 # A dialog will warn the user such files later.
1373 fn = os.path.normpath(fn)
1374 if fn not in g.app.already_open_files:
1375 g.es('may be open in another Leo:', color='red')
1376 g.es(fn)
1377 g.app.already_open_files.append(fn)
1378 else:
1379 g.app.rememberOpenFile(fn)
1380 #@+node:ekr.20120427064024.10066: *4* app.forgetOpenFile
1381 def forgetOpenFile(self, fn):
1382 """
1383 Remove fn from g.app.db, so that is no longer considered open.
1384 """
1385 trace = 'shutdown' in g.app.debug
1386 d, tag = g.app.db, 'open-leo-files'
1387 if not d or not fn:
1388 return # #69.
1389 aList = d.get(tag) or []
1390 fn = os.path.normpath(fn)
1391 if fn in aList:
1392 aList.remove(fn)
1393 if trace:
1394 g.pr(f"forgetOpenFile: {g.shortFileName(fn)}")
1395 d[tag] = aList
1397 #@+node:ekr.20120427064024.10065: *4* app.rememberOpenFile
1398 def rememberOpenFile(self, fn):
1400 #
1401 # Do not call g.trace, etc. here.
1402 d, tag = g.app.db, 'open-leo-files'
1403 if d is None or g.unitTesting or g.app.batchMode or g.app.reverting:
1404 pass
1405 elif g.app.preReadFlag:
1406 pass
1407 else:
1408 aList = d.get(tag) or []
1409 # It's proper to add duplicates to this list.
1410 aList.append(os.path.normpath(fn))
1411 d[tag] = aList
1412 #@+node:ekr.20150621062355.1: *4* app.runAlreadyOpenDialog
1413 def runAlreadyOpenDialog(self, c):
1414 """Warn about possibly already-open files."""
1415 if g.app.already_open_files:
1416 aList = sorted(set(g.app.already_open_files))
1417 g.app.already_open_files = []
1418 g.app.gui.dismiss_splash_screen()
1419 message = (
1420 'The following files may already be open\n'
1421 'in another copy of Leo:\n\n' +
1422 '\n'.join(aList))
1423 g.app.gui.runAskOkDialog(c,
1424 title='Already Open Files',
1425 message=message,
1426 text="Ok")
1427 #@+node:ekr.20171127111141.1: *3* app.Import utils
1428 #@+node:ekr.20140727180847.17985: *4* app.scanner_for_at_auto
1429 def scanner_for_at_auto(self, c, p, **kwargs):
1430 """A factory returning a scanner function for p, an @auto node."""
1431 d = g.app.atAutoDict
1432 for key in d:
1433 # pylint: disable=cell-var-from-loop
1434 func = d.get(key)
1435 if func and g.match_word(p.h, 0, key):
1436 return func
1437 return None
1438 #@+node:ekr.20140130172810.15471: *4* app.scanner_for_ext
1439 def scanner_for_ext(self, c, ext, **kwargs):
1440 """A factory returning a scanner function for the given file extension."""
1441 return g.app.classDispatchDict.get(ext)
1442 #@+node:ekr.20170429152049.1: *3* app.listenToLog
1443 @cmd('listen-to-log')
1444 @cmd('log-listen')
1445 def listenToLog(self, event=None):
1446 """
1447 A socket listener, listening on localhost. See:
1448 https://docs.python.org/2/howto/logging-cookbook.html#sending-and-receiving-logging-events-across-a-network
1450 Start this listener first, then start the broadcaster.
1452 leo/plugins/cursesGui2.py is a typical broadcaster.
1453 """
1454 app = self
1455 # Kill any previous listener.
1456 if app.log_listener:
1457 g.es_print('Killing previous listener')
1458 try:
1459 app.log_listener.kill()
1460 except Exception:
1461 g.es_exception()
1462 app.log_listener = None
1463 # Start a new listener.
1464 g.es_print('Starting log_listener.py')
1465 path = g.os_path_finalize_join(app.loadDir,
1466 '..', 'external', 'log_listener.py')
1467 app.log_listener = subprocess.Popen(
1468 [sys.executable, path],
1469 shell=False,
1470 universal_newlines=True,
1471 )
1472 #@+node:ekr.20171118024827.1: *3* app.makeAllBindings
1473 def makeAllBindings(self):
1474 """
1475 LeoApp.makeAllBindings:
1477 Call c.k.makeAllBindings for all open commanders c.
1478 """
1479 app = self
1480 for c in app.commanders():
1481 c.k.makeAllBindings()
1482 #@+node:ekr.20031218072017.2188: *3* app.newCommander
1483 def newCommander(self, fileName,
1484 gui=None,
1485 parentFrame=None,
1486 previousSettings=None,
1487 relativeFileName=None,
1488 ):
1489 """Create a commander and its view frame for the Leo main window."""
1490 # Create the commander and its subcommanders.
1491 # This takes about 3/4 sec when called by the leoBridge module.
1492 # Timeit reports 0.0175 sec when using a nullGui.
1493 from leo.core import leoCommands
1494 c = leoCommands.Commands(fileName,
1495 gui=gui,
1496 parentFrame=parentFrame,
1497 previousSettings=previousSettings,
1498 relativeFileName=relativeFileName,
1499 )
1500 return c
1501 #@+node:ekr.20120304065838.15588: *3* app.selectLeoWindow
1502 def selectLeoWindow(self, c):
1503 frame = c.frame
1504 frame.deiconify()
1505 frame.lift()
1506 c.setLog()
1507 master = getattr(frame.top, 'leo_master', None)
1508 if master:
1509 # master is a TabbedTopLevel.
1510 # Selecting the new tab ensures focus is set.
1511 master.select(c)
1512 if 1:
1513 c.initialFocusHelper()
1514 else:
1515 c.bodyWantsFocus()
1516 c.outerUpdate()
1517 #@-others
1518#@+node:ekr.20120209051836.10242: ** class LoadManager
1519class LoadManager:
1520 """A class to manage loading .leo files, including configuration files."""
1521 #@+others
1522 #@+node:ekr.20120214060149.15851: *3* LM.ctor
1523 def __init__(self):
1525 #
1526 # Global settings & shortcuts dicts...
1527 # The are the defaults for computing settings and shortcuts for all loaded files.
1528 #
1529 self.globalSettingsDict = None
1530 # A g.TypedDict: the join of settings in leoSettings.leo & myLeoSettings.leo
1531 self.globalBindingsDict = None
1532 # A g.TypedDict: the join of shortcuts in leoSettings.leo & myLeoSettings.leo.
1533 #
1534 # LoadManager ivars corresponding to user options...
1535 #
1536 self.files = []
1537 # List of files to be loaded.
1538 self.options = {}
1539 # Dictionary of user options. Keys are option names.
1540 self.old_argv = []
1541 # A copy of sys.argv for debugging.
1542 self.more_cmdline_files = False
1543 # True when more files remain on the command line to be
1544 # loaded. If the user is answering "No" to each file as Leo asks
1545 # "file already open, open again", this must be False for
1546 # a complete exit to be appropriate (finish_quit=True param for
1547 # closeLeoWindow())
1548 #
1549 # Themes.
1550 self.leo_settings_c = None
1551 self.leo_settings_path = None
1552 self.my_settings_c = None
1553 self.my_settings_path = None
1554 self.theme_c = None
1555 # #1374.
1556 self.theme_path = None
1557 #@+node:ekr.20120211121736.10812: *3* LM.Directory & file utils
1558 #@+node:ekr.20120219154958.10481: *4* LM.completeFileName
1559 def completeFileName(self, fileName):
1560 fileName = g.toUnicode(fileName)
1561 fileName = g.os_path_finalize(fileName)
1562 # 2011/10/12: don't add .leo to *any* file.
1563 return fileName
1564 #@+node:ekr.20120209051836.10372: *4* LM.computeLeoSettingsPath
1565 def computeLeoSettingsPath(self):
1566 """Return the full path to leoSettings.leo."""
1567 # lm = self
1568 join = g.os_path_finalize_join
1569 settings_fn = 'leoSettings.leo'
1570 table = (
1571 # First, leoSettings.leo in the home directories.
1572 join(g.app.homeDir, settings_fn),
1573 join(g.app.homeLeoDir, settings_fn),
1574 # Last, leoSettings.leo in leo/config directory.
1575 join(g.app.globalConfigDir, settings_fn)
1576 )
1577 for path in table:
1578 if g.os_path_exists(path):
1579 break
1580 else:
1581 path = None
1582 return path
1583 #@+node:ekr.20120209051836.10373: *4* LM.computeMyLeoSettingsPath
1584 def computeMyLeoSettingsPath(self):
1585 """
1586 Return the full path to myLeoSettings.leo.
1588 The "footnote": Get the local directory from lm.files[0]
1589 """
1590 lm = self
1591 join = g.os_path_finalize_join
1592 settings_fn = 'myLeoSettings.leo'
1593 # This seems pointless: we need a machine *directory*.
1594 # For now, however, we'll keep the existing code as is.
1595 machine_fn = lm.computeMachineName() + settings_fn
1596 # First, compute the directory of the first loaded file.
1597 # All entries in lm.files are full, absolute paths.
1598 localDir = g.os_path_dirname(lm.files[0]) if lm.files else ''
1599 table = (
1600 # First, myLeoSettings.leo in the local directory
1601 join(localDir, settings_fn),
1602 # Next, myLeoSettings.leo in the home directories.
1603 join(g.app.homeDir, settings_fn),
1604 join(g.app.homeLeoDir, settings_fn),
1605 # Next, <machine-name>myLeoSettings.leo in the home directories.
1606 join(g.app.homeDir, machine_fn),
1607 join(g.app.homeLeoDir, machine_fn),
1608 # Last, leoSettings.leo in leo/config directory.
1609 join(g.app.globalConfigDir, settings_fn),
1610 )
1611 for path in table:
1612 if g.os_path_exists(path):
1613 break
1614 else:
1615 path = None
1616 return path
1617 #@+node:ekr.20120209051836.10252: *4* LM.computeStandardDirectories & helpers
1618 def computeStandardDirectories(self):
1619 """
1620 Compute the locations of standard directories and
1621 set the corresponding ivars.
1622 """
1623 lm = self
1624 join = os.path.join
1625 g.app.loadDir = lm.computeLoadDir()
1626 g.app.globalConfigDir = lm.computeGlobalConfigDir()
1627 g.app.homeDir = lm.computeHomeDir()
1628 g.app.homeLeoDir = lm.computeHomeLeoDir()
1629 g.app.leoDir = lm.computeLeoDir()
1630 # These use g.app.loadDir...
1631 g.app.extensionsDir = join(g.app.loadDir, '..', 'extensions')
1632 g.app.leoEditorDir = join(g.app.loadDir, '..', '..')
1633 g.app.testDir = join(g.app.loadDir, '..', 'test')
1634 #@+node:ekr.20120209051836.10253: *5* LM.computeGlobalConfigDir
1635 def computeGlobalConfigDir(self):
1636 leo_config_dir = getattr(sys, 'leo_config_directory', None)
1637 if leo_config_dir:
1638 theDir = leo_config_dir
1639 else:
1640 theDir = os.path.join(g.app.loadDir, "..", "config")
1641 if theDir:
1642 theDir = os.path.abspath(theDir)
1643 if not theDir or not g.os_path_exists(theDir) or not g.os_path_isdir(theDir):
1644 theDir = None
1645 return theDir
1646 #@+node:ekr.20120209051836.10254: *5* LM.computeHomeDir
1647 def computeHomeDir(self):
1648 """Returns the user's home directory."""
1649 home = os.path.expanduser("~")
1650 # Windows searches the HOME, HOMEPATH and HOMEDRIVE
1651 # environment vars, then gives up.
1652 if home and len(home) > 1 and home[0] == '%' and home[-1] == '%':
1653 # Get the indirect reference to the true home.
1654 home = os.getenv(home[1:-1], default=None)
1655 if home:
1656 # Important: This returns the _working_ directory if home is None!
1657 # This was the source of the 4.3 .leoID.txt problems.
1658 home = g.os_path_finalize(home)
1659 if (not g.os_path_exists(home) or not g.os_path_isdir(home)):
1660 home = None
1661 return home
1662 #@+node:ekr.20120209051836.10260: *5* LM.computeHomeLeoDir
1663 def computeHomeLeoDir(self):
1664 # lm = self
1665 homeLeoDir = g.os_path_finalize_join(g.app.homeDir, '.leo')
1666 if g.os_path_exists(homeLeoDir):
1667 return homeLeoDir
1668 ok = g.makeAllNonExistentDirectories(homeLeoDir)
1669 return homeLeoDir if ok else '' # #1450
1670 #@+node:ekr.20120209051836.10255: *5* LM.computeLeoDir
1671 def computeLeoDir(self):
1672 # lm = self
1673 loadDir = g.app.loadDir
1674 return g.os_path_dirname(loadDir)
1675 # We don't want the result in sys.path
1676 #@+node:ekr.20120209051836.10256: *5* LM.computeLoadDir
1677 def computeLoadDir(self):
1678 """Returns the directory containing leo.py."""
1679 try:
1680 # Fix a hangnail: on Windows the drive letter returned by
1681 # __file__ is randomly upper or lower case!
1682 # The made for an ugly recent files list.
1683 path = g.__file__ # was leo.__file__
1684 if path:
1685 # Possible fix for bug 735938:
1686 # Do the following only if path exists.
1687 #@+<< resolve symlinks >>
1688 #@+node:ekr.20120209051836.10257: *6* << resolve symlinks >>
1689 if path.endswith('pyc'):
1690 srcfile = path[:-1]
1691 if os.path.islink(srcfile):
1692 path = os.path.realpath(srcfile)
1693 #@-<< resolve symlinks >>
1694 if sys.platform == 'win32':
1695 if len(path) > 2 and path[1] == ':':
1696 # Convert the drive name to upper case.
1697 path = path[0].upper() + path[1:]
1698 path = g.os_path_finalize(path)
1699 loadDir = g.os_path_dirname(path)
1700 else: loadDir = None
1701 if (
1702 not loadDir or
1703 not g.os_path_exists(loadDir) or
1704 not g.os_path_isdir(loadDir)
1705 ):
1706 loadDir = os.getcwd()
1707 # From Marc-Antoine Parent.
1708 if loadDir.endswith("Contents/Resources"):
1709 loadDir += "/leo/plugins"
1710 else:
1711 g.pr("Exception getting load directory")
1712 loadDir = g.os_path_finalize(loadDir)
1713 return loadDir
1714 except Exception:
1715 print("Exception getting load directory")
1716 raise
1717 #@+node:ekr.20120213164030.10697: *5* LM.computeMachineName
1718 def computeMachineName(self):
1719 """Return the name of the current machine, i.e, HOSTNAME."""
1720 # This is prepended to leoSettings.leo or myLeoSettings.leo
1721 # to give the machine-specific setting name.
1722 # How can this be worth doing??
1723 try:
1724 name = os.getenv('HOSTNAME')
1725 if not name:
1726 name = os.getenv('COMPUTERNAME')
1727 if not name:
1728 import socket
1729 name = socket.gethostname()
1730 except Exception:
1731 name = ''
1732 return name
1733 #@+node:ekr.20180318120148.1: *4* LM.computeThemeDirectories
1734 def computeThemeDirectories(self):
1735 """
1736 Return a list of *existing* directories that might contain theme .leo files.
1737 """
1738 join = g.os_path_finalize_join
1739 home = g.app.homeDir
1740 leo = join(g.app.loadDir, '..')
1741 table = [
1742 home,
1743 join(home, 'themes'),
1744 join(home, '.leo'),
1745 join(home, '.leo', 'themes'),
1746 join(leo, 'themes'),
1747 ]
1748 return [g.os_path_normslashes(z) for z in table if g.os_path_exists(z)]
1749 # Make sure home has normalized slashes.
1750 #@+node:ekr.20180318133620.1: *4* LM.computeThemeFilePath & helper
1751 def computeThemeFilePath(self):
1752 """
1753 Return the absolute path to the theme .leo file, resolved using the search order for themes.
1755 1. Use the --theme command-line option if it exists.
1757 2. Otherwise, preload the first .leo file.
1758 Load the file given by @string theme-name setting.
1760 3. Finally, look up the @string theme-name in the already-loaded, myLeoSettings.leo.
1761 Load the file if setting exists. Otherwise return None.
1762 """
1763 trace = 'themes' in g.app.db
1764 lm = self
1765 resolve = self.resolve_theme_path
1766 #
1767 # Step 1: Use the --theme command-line options if it exists
1768 path = resolve(lm.options.get('theme_path'), tag='--theme')
1769 if path:
1770 # Caller (LM.readGlobalSettingsFiles) sets lm.theme_path
1771 if trace:
1772 g.trace('--theme:', path)
1773 return path
1774 #
1775 # Step 2: look for the @string theme-name setting in the first loaded file.
1776 path = lm.files and lm.files[0]
1777 if path and g.os_path_exists(path):
1778 # Tricky: we must call lm.computeLocalSettings *here*.
1779 theme_c = lm.openSettingsFile(path)
1780 if theme_c:
1781 settings_d, junk_shortcuts_d = lm.computeLocalSettings(
1782 c=theme_c,
1783 settings_d=lm.globalSettingsDict,
1784 bindings_d=lm.globalBindingsDict,
1785 localFlag=False,
1786 )
1787 setting = settings_d.get_string_setting('theme-name')
1788 if setting:
1789 tag = theme_c.shortFileName()
1790 path = resolve(setting, tag=tag)
1791 if path:
1792 # Caller (LM.readGlobalSettingsFiles) sets lm.theme_path
1793 if trace:
1794 g.trace("First loaded file", theme_c.shortFileName(), path)
1795 return path
1796 #
1797 # Step 3: use the @string theme-name setting in myLeoSettings.leo.
1798 # Note: the setting should *never* appear in leoSettings.leo!
1799 setting = lm.globalSettingsDict.get_string_setting('theme-name')
1800 tag = 'myLeoSettings.leo'
1801 path = resolve(setting, tag=tag)
1802 if trace:
1803 g.trace("myLeoSettings.leo", path)
1804 return path
1805 #@+node:ekr.20180321124503.1: *5* LM.resolve_theme_path
1806 def resolve_theme_path(self, fn, tag):
1807 """Search theme directories for the given .leo file."""
1808 if not fn or fn.lower().strip() == 'none':
1809 return None
1810 if not fn.endswith('.leo'):
1811 fn += '.leo'
1812 for directory in self.computeThemeDirectories():
1813 path = g.os_path_join(directory, fn)
1814 # Normalizes slashes, etc.
1815 if g.os_path_exists(path):
1816 return path
1817 print(f"theme .leo file not found: {fn}")
1818 return None
1819 #@+node:ekr.20120211121736.10772: *4* LM.computeWorkbookFileName
1820 def computeWorkbookFileName(self):
1821 """
1822 Return full path to the workbook.
1824 Return None if testing, or in batch mode, or if the containing
1825 directory does not exist.
1826 """
1827 # lm = self
1828 # Never create a workbook during unit tests or in batch mode.
1829 if g.unitTesting or g.app.batchMode:
1830 return None
1831 fn = g.app.config.getString(setting='default_leo_file') or '~/.leo/workbook.leo'
1832 fn = g.os_path_finalize(fn)
1833 directory = g.os_path_finalize(os.path.dirname(fn))
1834 # #1415.
1835 return fn if os.path.exists(directory) else None
1836 #@+node:ekr.20120219154958.10485: *4* LM.reportDirectories
1837 def reportDirectories(self):
1838 """Report directories."""
1839 # The cwd changes later, so it would be misleading to report it here.
1840 for kind, theDir in (
1841 ('home', g.app.homeDir),
1842 ('leo-editor', g.app.leoEditorDir),
1843 ('load', g.app.loadDir),
1844 ('config', g.app.globalConfigDir),
1845 ):
1846 # g.blue calls g.es_print, and that's annoying.
1847 g.es(f"{kind:>10}:", os.path.normpath(theDir), color='blue')
1848 #@+node:ekr.20120215062153.10740: *3* LM.Settings
1849 #@+node:ekr.20120130101219.10182: *4* LM.computeBindingLetter
1850 def computeBindingLetter(self, c, path):
1851 lm = self
1852 if not path:
1853 return 'D'
1854 path = path.lower()
1855 table = (
1856 ('M', 'myLeoSettings.leo'),
1857 (' ', 'leoSettings.leo'),
1858 ('F', c.shortFileName()),
1859 )
1860 for letter, path2 in table:
1861 if path2 and path.endswith(path2.lower()):
1862 return letter
1863 if lm.theme_path and path.endswith(lm.theme_path.lower()):
1864 return 'T'
1865 if path == 'register-command' or path.find('mode') > -1:
1866 return '@'
1867 return 'D'
1868 #@+node:ekr.20120223062418.10421: *4* LM.computeLocalSettings
1869 def computeLocalSettings(self, c, settings_d, bindings_d, localFlag):
1870 """
1871 Merge the settings dicts from c's outline into *new copies of*
1872 settings_d and bindings_d.
1873 """
1874 lm = self
1875 shortcuts_d2, settings_d2 = lm.createSettingsDicts(c, localFlag)
1876 if not bindings_d: # #1766: unit tests.
1877 settings_d, bindings_d = lm.createDefaultSettingsDicts()
1878 if settings_d2:
1879 if g.app.trace_setting:
1880 key = g.app.config.munge(g.app.trace_setting)
1881 val = settings_d2.d.get(key)
1882 if val:
1883 fn = g.shortFileName(val.path)
1884 g.es_print(
1885 f"--trace-setting: in {fn:20}: "
1886 f"@{val.kind} {g.app.trace_setting}={val.val}")
1887 settings_d = settings_d.copy()
1888 settings_d.update(settings_d2)
1889 if shortcuts_d2:
1890 bindings_d = lm.mergeShortcutsDicts(c, bindings_d, shortcuts_d2, localFlag)
1891 return settings_d, bindings_d
1892 #@+node:ekr.20121126202114.3: *4* LM.createDefaultSettingsDicts
1893 def createDefaultSettingsDicts(self):
1894 """Create lm.globalSettingsDict & lm.globalBindingsDict."""
1895 settings_d = g.app.config.defaultsDict
1896 assert isinstance(settings_d, g.TypedDict), settings_d
1897 settings_d.setName('lm.globalSettingsDict')
1898 bindings_d = g.TypedDict( # was TypedDictOfLists.
1899 name='lm.globalBindingsDict',
1900 keyType=type('s'),
1901 valType=g.BindingInfo,
1902 )
1903 return settings_d, bindings_d
1904 #@+node:ekr.20120214165710.10726: *4* LM.createSettingsDicts
1905 def createSettingsDicts(self, c, localFlag):
1907 from leo.core import leoConfig
1908 if c:
1909 parser = leoConfig.SettingsTreeParser(c, localFlag)
1910 # returns the *raw* shortcutsDict, not a *merged* shortcuts dict.
1911 shortcutsDict, settingsDict = parser.traverse()
1912 return shortcutsDict, settingsDict
1913 return None, None
1914 #@+node:ekr.20120223062418.10414: *4* LM.getPreviousSettings
1915 def getPreviousSettings(self, fn):
1916 """
1917 Return the settings in effect for fn. Typically, this involves
1918 pre-reading fn.
1919 """
1920 lm = self
1921 settingsName = f"settings dict for {g.shortFileName(fn)}"
1922 shortcutsName = f"shortcuts dict for {g.shortFileName(fn)}"
1923 # A special case: settings in leoSettings.leo do *not* override
1924 # the global settings, that is, settings in myLeoSettings.leo.
1925 isLeoSettings = g.shortFileName(fn).lower() == 'leosettings.leo'
1926 exists = g.os_path_exists(fn)
1927 if fn and exists and lm.isLeoFile(fn) and not isLeoSettings:
1928 # Open the file usinging a null gui.
1929 try:
1930 g.app.preReadFlag = True
1931 c = lm.openSettingsFile(fn)
1932 finally:
1933 g.app.preReadFlag = False
1934 # Merge the settings from c into *copies* of the global dicts.
1935 d1, d2 = lm.computeLocalSettings(c,
1936 lm.globalSettingsDict,
1937 lm.globalBindingsDict,
1938 localFlag=True)
1939 # d1 and d2 are copies.
1940 d1.setName(settingsName)
1941 d2.setName(shortcutsName)
1942 return PreviousSettings(d1, d2)
1943 #
1944 # The file does not exist, or is not valid.
1945 # Get the settings from the globals settings dicts.
1946 if lm.globalSettingsDict and lm.globalBindingsDict: # #1766.
1947 d1 = lm.globalSettingsDict.copy(settingsName)
1948 d2 = lm.globalBindingsDict.copy(shortcutsName)
1949 else:
1950 d1 = d2 = None
1951 return PreviousSettings(d1, d2)
1952 #@+node:ekr.20120214132927.10723: *4* LM.mergeShortcutsDicts & helpers
1953 def mergeShortcutsDicts(self, c, old_d, new_d, localFlag):
1954 """
1955 Create a new dict by overriding all shortcuts in old_d by shortcuts in new_d.
1957 Both old_d and new_d remain unchanged.
1958 """
1959 lm = self
1960 if not old_d:
1961 return new_d
1962 if not new_d:
1963 return old_d
1964 bi_list = new_d.get(g.app.trace_setting)
1965 if bi_list:
1966 # This code executed only if g.app.trace_setting exists.
1967 for bi in bi_list:
1968 fn = bi.kind.split(' ')[-1]
1969 stroke = c.k.prettyPrintKey(bi.stroke)
1970 if bi.pane and bi.pane != 'all':
1971 pane = f" in {bi.pane} panes"
1972 else:
1973 pane = ''
1974 inverted_old_d = lm.invert(old_d)
1975 inverted_new_d = lm.invert(new_d)
1976 # #510 & #327: always honor --trace-binding here.
1977 if g.app.trace_binding:
1978 binding = g.app.trace_binding
1979 # First, see if the binding is for a command. (Doesn't work for plugin commands).
1980 if localFlag and binding in c.k.killedBindings:
1981 g.es_print(
1982 f"--trace-binding: {c.shortFileName()} "
1983 f"sets {binding} to None")
1984 elif localFlag and binding in c.commandsDict:
1985 d = c.k.computeInverseBindingDict()
1986 g.trace(
1987 f"--trace-binding: {c.shortFileName():20} "
1988 f"binds {binding} to {d.get(binding) or []}")
1989 else:
1990 binding = g.app.trace_binding
1991 stroke = g.KeyStroke(binding)
1992 bi_list = inverted_new_d.get(stroke)
1993 if bi_list:
1994 print('')
1995 for bi in bi_list:
1996 fn = bi.kind.split(' ')[-1] # bi.kind #
1997 stroke2 = c.k.prettyPrintKey(stroke)
1998 if bi.pane and bi.pane != 'all':
1999 pane = f" in {bi.pane} panes"
2000 else:
2001 pane = ''
2002 g.es_print(
2003 f"--trace-binding: {fn:20} binds {stroke2} "
2004 f"to {bi.commandName:>20}{pane}")
2005 print('')
2006 # Fix bug 951921: check for duplicate shortcuts only in the new file.
2007 lm.checkForDuplicateShortcuts(c, inverted_new_d)
2008 inverted_old_d.update(inverted_new_d) # Updates inverted_old_d in place.
2009 result = lm.uninvert(inverted_old_d)
2010 return result
2011 #@+node:ekr.20120311070142.9904: *5* LM.checkForDuplicateShortcuts
2012 def checkForDuplicateShortcuts(self, c, d):
2013 """
2014 Check for duplicates in an "inverted" dictionary d
2015 whose keys are strokes and whose values are lists of BindingInfo nodes.
2017 Duplicates happen only if panes conflict.
2018 """
2019 # lm = self
2020 # Fix bug 951921: check for duplicate shortcuts only in the new file.
2021 for ks in sorted(list(d.keys())):
2022 duplicates, panes = [], ['all']
2023 aList = d.get(ks)
2024 # A list of bi objects.
2025 aList2 = [z for z in aList if not z.pane.startswith('mode')]
2026 if len(aList) > 1:
2027 for bi in aList2:
2028 if bi.pane in panes:
2029 duplicates.append(bi)
2030 else:
2031 panes.append(bi.pane)
2032 if duplicates:
2033 bindings = list(set([z.stroke.s for z in duplicates]))
2034 if len(bindings) == 1:
2035 kind = 'duplicate, (not conflicting)'
2036 else:
2037 kind = 'conflicting'
2038 g.es_print(f"{kind} key bindings in {c.shortFileName()}")
2039 for bi in aList2:
2040 g.es_print(f"{bi.pane:6} {bi.stroke.s} {bi.commandName}")
2041 #@+node:ekr.20120214132927.10724: *5* LM.invert
2042 def invert(self, d):
2043 """
2044 Invert a shortcut dict whose keys are command names,
2045 returning a dict whose keys are strokes.
2046 """
2047 result = g.TypedDict( # was TypedDictOfLists.
2048 name=f"inverted {d.name()}",
2049 keyType=g.KeyStroke,
2050 valType=g.BindingInfo,
2051 )
2052 for commandName in d.keys():
2053 for bi in d.get(commandName, []):
2054 stroke = bi.stroke # This is canonicalized.
2055 bi.commandName = commandName # Add info.
2056 assert stroke
2057 result.add_to_list(stroke, bi)
2058 return result
2059 #@+node:ekr.20120214132927.10725: *5* LM.uninvert
2060 def uninvert(self, d):
2061 """
2062 Uninvert an inverted shortcut dict whose keys are strokes,
2063 returning a dict whose keys are command names.
2064 """
2065 assert d.keyType == g.KeyStroke, d.keyType
2066 result = g.TypedDict( # was TypedDictOfLists.
2067 name=f"uninverted {d.name()}",
2068 keyType=type('commandName'),
2069 valType=g.BindingInfo,
2070 )
2071 for stroke in d.keys():
2072 for bi in d.get(stroke, []):
2073 commandName = bi.commandName
2074 assert commandName
2075 result.add_to_list(commandName, bi)
2076 return result
2077 #@+node:ekr.20120222103014.10312: *4* LM.openSettingsFile
2078 def openSettingsFile(self, fn):
2079 """
2080 Open a settings file with a null gui. Return the commander.
2082 The caller must init the c.config object.
2083 """
2084 lm = self
2085 if not fn:
2086 return None
2087 theFile = lm.openAnyLeoFile(fn)
2088 if not theFile:
2089 return None # Fix #843.
2090 if not any([g.unitTesting, g.app.silentMode, g.app.batchMode]):
2091 # This occurs early in startup, so use the following.
2092 s = f"reading settings in {os.path.normpath(fn)}"
2093 if 'startup' in g.app.debug:
2094 print(s)
2095 g.es(s, color='blue')
2096 # A useful trace.
2097 # g.trace('%20s' % g.shortFileName(fn), g.callers(3))
2098 # Changing g.app.gui here is a major hack. It is necessary.
2099 oldGui = g.app.gui
2100 g.app.gui = g.app.nullGui
2101 c = g.app.newCommander(fn)
2102 frame = c.frame
2103 frame.log.enable(False)
2104 g.app.lockLog()
2105 g.app.openingSettingsFile = True
2106 try:
2107 ok = c.fileCommands.openLeoFile(theFile, fn,
2108 readAtFileNodesFlag=False, silent=True)
2109 # closes theFile.
2110 finally:
2111 g.app.openingSettingsFile = False
2112 g.app.unlockLog()
2113 c.openDirectory = frame.openDirectory = g.os_path_dirname(fn)
2114 g.app.gui = oldGui
2115 return c if ok else None
2116 #@+node:ekr.20120213081706.10382: *4* LM.readGlobalSettingsFiles
2117 def readGlobalSettingsFiles(self):
2118 """
2119 Read leoSettings.leo and myLeoSettings.leo using a null gui.
2121 New in Leo 6.1: this sets ivars for the ActiveSettingsOutline class.
2122 """
2123 trace = 'themes' in g.app.debug
2124 lm = self
2125 # Open the standard settings files with a nullGui.
2126 # Important: their commanders do not exist outside this method!
2127 old_commanders = g.app.commanders()
2128 lm.leo_settings_path = lm.computeLeoSettingsPath()
2129 lm.my_settings_path = lm.computeMyLeoSettingsPath()
2130 lm.leo_settings_c = lm.openSettingsFile(self.leo_settings_path)
2131 lm.my_settings_c = lm.openSettingsFile(self.my_settings_path)
2132 commanders = [lm.leo_settings_c, lm.my_settings_c]
2133 commanders = [z for z in commanders if z]
2134 settings_d, bindings_d = lm.createDefaultSettingsDicts()
2135 for c in commanders:
2136 # Merge the settings dicts from c's outline into
2137 # *new copies of* settings_d and bindings_d.
2138 settings_d, bindings_d = lm.computeLocalSettings(
2139 c, settings_d, bindings_d, localFlag=False)
2140 # Adjust the name.
2141 bindings_d.setName('lm.globalBindingsDict')
2142 lm.globalSettingsDict = settings_d
2143 lm.globalBindingsDict = bindings_d
2144 # Add settings from --theme or @string theme-name files.
2145 # This must be done *after* reading myLeoSettigns.leo.
2146 lm.theme_path = lm.computeThemeFilePath()
2147 if lm.theme_path:
2148 lm.theme_c = lm.openSettingsFile(lm.theme_path)
2149 if lm.theme_c:
2150 # Merge theme_c's settings into globalSettingsDict.
2151 settings_d, junk_shortcuts_d = lm.computeLocalSettings(
2152 lm.theme_c, settings_d, bindings_d, localFlag=False)
2153 lm.globalSettingsDict = settings_d
2154 # Set global vars
2155 g.app.theme_directory = g.os_path_dirname(lm.theme_path)
2156 # Used by the StyleSheetManager.
2157 if trace:
2158 g.trace('g.app.theme_directory', g.app.theme_directory)
2159 # Clear the cache entries for the commanders.
2160 # This allows this method to be called outside the startup logic.
2161 for c in commanders:
2162 if c not in old_commanders:
2163 g.app.forgetOpenFile(c.fileName())
2164 #@+node:ekr.20120214165710.10838: *4* LM.traceSettingsDict
2165 def traceSettingsDict(self, d, verbose=False):
2166 if verbose:
2167 print(d)
2168 for key in sorted(list(d.keys())):
2169 gs = d.get(key)
2170 print(f"{key:35} {g.shortFileName(gs.path):17} {gs.val}")
2171 if d:
2172 print('')
2173 else:
2174 # print(d)
2175 print(f"{d.name} {len(d.d.keys())}")
2176 #@+node:ekr.20120214165710.10822: *4* LM.traceShortcutsDict
2177 def traceShortcutsDict(self, d, verbose=False):
2178 if verbose:
2179 print(d)
2180 for key in sorted(list(d.keys())):
2181 val = d.get(key)
2182 # print('%20s %s' % (key,val.dump()))
2183 print(f"{key:35} {[z.stroke for z in val]}")
2184 if d:
2185 print('')
2186 else:
2187 print(d)
2188 #@+node:ekr.20120219154958.10452: *3* LM.load & helpers
2189 def load(self, fileName=None, pymacs=None):
2190 """This is Leo's main startup method."""
2191 lm = self
2192 #
2193 # Phase 1: before loading plugins.
2194 # Scan options, set directories and read settings.
2195 t1 = time.process_time()
2196 print('') # Give some separation for the coming traces.
2197 if not lm.isValidPython():
2198 return
2199 lm.doPrePluginsInit(fileName, pymacs)
2200 # sets lm.options and lm.files
2201 g.app.computeSignon()
2202 g.app.printSignon()
2203 if lm.options.get('version'):
2204 return
2205 if not g.app.gui:
2206 return
2207 g.app.disable_redraw = True
2208 # Disable redraw until all files are loaded.
2209 #
2210 # Phase 2: load plugins: the gui has already been set.
2211 t2 = time.process_time()
2212 g.doHook("start1")
2213 t3 = time.process_time()
2214 if g.app.killed:
2215 return
2216 g.app.idleTimeManager.start()
2217 #
2218 # Phase 3: after loading plugins. Create one or more frames.
2219 t3 = time.process_time()
2220 if lm.options.get('script') and not self.files:
2221 ok = True
2222 else:
2223 ok = lm.doPostPluginsInit()
2224 # Fix #579: Key bindings don't take for commands defined in plugins
2225 g.app.makeAllBindings()
2226 if ok and g.app.diff:
2227 lm.doDiff()
2228 if not ok:
2229 return
2230 g.es('') # Clears horizontal scrolling in the log pane.
2231 if g.app.listen_to_log_flag:
2232 g.app.listenToLog()
2233 if 'startup' in g.app.debug:
2234 t4 = time.process_time()
2235 print('')
2236 g.es_print(f"settings:{t2 - t1:5.2f} sec")
2237 g.es_print(f" plugins:{t3 - t2:5.2f} sec")
2238 g.es_print(f" files:{t4 - t3:5.2f} sec")
2239 g.es_print(f" total:{t4 - t1:5.2f} sec")
2240 print('')
2241 # -- quit
2242 if g.app.quit_after_load:
2243 if 'shutdown' in g.app.debug or 'startup' in g.app.debug:
2244 print('--quit')
2245 g.app.forceShutdown()
2246 return
2247 # #1128: support for restart-leo.
2248 if not g.app.start_minimized:
2249 try: # Careful: we may be unit testing.
2250 g.app.log.c.frame.bringToFront()
2251 except Exception:
2252 pass
2253 g.app.gui.runMainLoop()
2254 # For scripts, the gui is a nullGui.
2255 # and the gui.setScript has already been called.
2256 #@+node:ekr.20150225133846.7: *4* LM.doDiff
2257 def doDiff(self):
2258 """Support --diff option after loading Leo."""
2259 if len(self.old_argv[2:]) == 2:
2260 pass # c.editFileCommands.compareAnyTwoFiles gives a message.
2261 else:
2262 # This is an unusual situation.
2263 g.es('--diff mode. sys.argv[2:]...', color='red')
2264 for z in self.old_argv[2:]:
2265 g.es(g.shortFileName(z) if z else repr(z), color='blue')
2266 commanders = g.app.commanders()
2267 if len(commanders) == 2:
2268 c = commanders[0]
2269 c.editFileCommands.compareAnyTwoFiles(event=None)
2270 #@+node:ekr.20120219154958.10487: *4* LM.doPostPluginsInit & helpers
2271 def doPostPluginsInit(self):
2272 """Create a Leo window for each file in the lm.files list."""
2273 # Clear g.app.initing _before_ creating commanders.
2274 lm = self
2275 g.app.initing = False # "idle" hooks may now call g.app.forceShutdown.
2276 # Create the main frame.Show it and all queued messages.
2277 c = c1 = fn = None
2278 if lm.files:
2279 try: # #1403.
2280 for n, fn in enumerate(lm.files):
2281 lm.more_cmdline_files = n < len(lm.files) - 1
2282 c = lm.loadLocalFile(fn, gui=g.app.gui, old_c=None)
2283 # Returns None if the file is open in another instance of Leo.
2284 if c and not c1: # #1416:
2285 c1 = c
2286 except Exception:
2287 g.es_print(f"Unexpected exception reading {fn!r}")
2288 g.es_exception()
2289 c = None
2290 # Load (and save later) a session *only* if the command line contains no files.
2291 g.app.loaded_session = not lm.files
2292 if g.app.sessionManager and g.app.loaded_session:
2293 try: # #1403.
2294 aList = g.app.sessionManager.load_snapshot()
2295 if aList:
2296 g.app.sessionManager.load_session(c1, aList)
2297 # #659.
2298 if g.app.windowList:
2299 c = c1 = g.app.windowList[0].c
2300 else:
2301 c = c1 = None
2302 except Exception:
2303 g.es_print('Can not load session')
2304 g.es_exception()
2305 # Enable redraws.
2306 g.app.disable_redraw = False
2307 if not c1:
2308 try: # #1403.
2309 c1 = lm.openEmptyWorkBook()
2310 # Calls LM.loadLocalFile.
2311 except Exception:
2312 g.es_print('Can not create empty workbook')
2313 g.es_exception()
2314 c = c1
2315 if not c:
2316 # Leo is out of options: Force an immediate exit.
2317 return False
2318 # #199.
2319 g.app.runAlreadyOpenDialog(c1)
2320 #
2321 # Final inits...
2322 # For qt gui, select the first-loaded tab.
2323 if hasattr(g.app.gui, 'frameFactory'):
2324 factory = g.app.gui.frameFactory
2325 if factory and hasattr(factory, 'setTabForCommander'):
2326 factory.setTabForCommander(c)
2327 g.app.logInited = True
2328 g.app.initComplete = True
2329 c.setLog()
2330 c.redraw()
2331 g.doHook("start2", c=c, p=c.p, fileName=c.fileName())
2332 c.initialFocusHelper()
2333 screenshot_fn = lm.options.get('screenshot_fn')
2334 if screenshot_fn:
2335 lm.make_screen_shot(screenshot_fn)
2336 return False # Force an immediate exit.
2337 return True
2338 #@+node:ekr.20120219154958.10489: *5* LM.make_screen_shot
2339 def make_screen_shot(self, fn):
2340 """Create a screenshot of the present Leo outline and save it to path."""
2341 if g.app.gui.guiName() == 'qt':
2342 m = g.loadOnePlugin('screenshots')
2343 m.make_screen_shot(fn)
2344 #@+node:ekr.20131028155339.17098: *5* LM.openEmptyWorkBook
2345 def openEmptyWorkBook(self):
2346 """Open an empty frame and paste the contents of CheatSheet.leo into it."""
2347 lm = self
2348 # Create an empty frame.
2349 fn = lm.computeWorkbookFileName()
2350 if not fn:
2351 return None # #1415
2352 c = lm.loadLocalFile(fn, gui=g.app.gui, old_c=None)
2353 if not c:
2354 return None # #1201: AttributeError below.
2355 if g.app.batchMode or g.os_path_exists(fn):
2356 return c
2357 # Open the cheatsheet.
2358 fn = g.os_path_finalize_join(g.app.loadDir, '..', 'doc', 'CheatSheet.leo')
2359 if not g.os_path_exists(fn):
2360 g.es(f"file not found: {fn}")
2361 return None
2362 # Paste the contents of CheetSheet.leo into c.
2363 old_clipboard = g.app.gui.getTextFromClipboard() # #933: Save clipboard.
2364 c2 = g.openWithFileName(fn, old_c=c)
2365 for p2 in c2.rootPosition().self_and_siblings():
2366 c2.setCurrentPosition(p2) # 1380
2367 c2.copyOutline()
2368 # #1380 & #1381: Add guard & use vnode methods to prevent redraw.
2369 p = c.pasteOutline()
2370 if p:
2371 c.setCurrentPosition(p) # 1380
2372 p.v.contract()
2373 p.v.clearDirty()
2374 c2.close(new_c=c)
2375 # Delete the dummy first node.
2376 root = c.rootPosition()
2377 root.doDelete(newNode=root.next())
2378 c.target_language = 'rest'
2379 c.clearChanged()
2380 c.redraw(c.rootPosition()) # # 1380: Select the root.
2381 g.app.gui.replaceClipboardWith(old_clipboard) # #933: Restore clipboard
2382 return c
2383 #@+node:ekr.20120219154958.10477: *4* LM.doPrePluginsInit & helpers
2384 def doPrePluginsInit(self, fileName, pymacs):
2385 """ Scan options, set directories and read settings."""
2386 lm = self
2387 lm.computeStandardDirectories()
2388 # Scan the options as early as possible.
2389 lm.options = options = lm.scanOptions(fileName, pymacs)
2390 # also sets lm.files.
2391 if options.get('version'):
2392 return
2393 script = options.get('script')
2394 verbose = script is None
2395 # Init the app.
2396 lm.initApp(verbose)
2397 g.app.setGlobalDb()
2398 if verbose:
2399 lm.reportDirectories()
2400 # Read settings *after* setting g.app.config and *before* opening plugins.
2401 # This means if-gui has effect only in per-file settings.
2402 if g.app.quit_after_load:
2403 localConfigFile = None
2404 else:
2405 lm.readGlobalSettingsFiles()
2406 # reads only standard settings files, using a null gui.
2407 # uses lm.files[0] to compute the local directory
2408 # that might contain myLeoSettings.leo.
2409 # Read the recent files file.
2410 localConfigFile = lm.files[0] if lm.files else None
2411 g.app.recentFilesManager.readRecentFiles(localConfigFile)
2412 # Create the gui after reading options and settings.
2413 lm.createGui(pymacs)
2414 # We can't print the signon until we know the gui.
2415 g.app.computeSignon() # Set app.signon/signon1 for commanders.
2416 #@+node:ekr.20170302093006.1: *5* LM.createAllImporterData & helpers
2417 def createAllImporterData(self):
2418 """
2419 New in Leo 5.5:
2421 Create global data structures describing importers and writers.
2422 """
2423 assert g.app.loadDir
2424 # This is the only data required.
2425 self.createWritersData()
2426 # Was an AtFile method.
2427 self.createImporterData()
2428 # Was a LeoImportCommands method.
2429 #@+node:ekr.20140724064952.18037: *6* LM.createImporterData & helper
2430 def createImporterData(self):
2431 """Create the data structures describing importer plugins."""
2432 # Allow plugins to be defined in ~/.leo/plugins.
2433 plugins1 = g.os_path_finalize_join(g.app.homeDir, '.leo', 'plugins')
2434 plugins2 = g.os_path_finalize_join(g.app.loadDir, '..', 'plugins')
2435 for kind, plugins in (('home', plugins1), ('leo', plugins2)):
2436 pattern = g.os_path_finalize_join(
2437 g.app.loadDir, '..', 'plugins', 'importers', '*.py')
2438 for fn in g.glob_glob(pattern):
2439 sfn = g.shortFileName(fn)
2440 if sfn != '__init__.py':
2441 try:
2442 module_name = sfn[:-3]
2443 # Important: use importlib to give imported modules
2444 # their fully qualified names.
2445 m = importlib.import_module(
2446 f"leo.plugins.importers.{module_name}")
2447 self.parse_importer_dict(sfn, m)
2448 # print('createImporterData', m.__name__)
2449 except Exception:
2450 g.warning(f"can not import leo.plugins.importers.{module_name}")
2451 #@+node:ekr.20140723140445.18076: *7* LM.parse_importer_dict
2452 def parse_importer_dict(self, sfn, m):
2453 """
2454 Set entries in g.app.classDispatchDict, g.app.atAutoDict and
2455 g.app.atAutoNames using entries in m.importer_dict.
2456 """
2457 importer_d = getattr(m, 'importer_dict', None)
2458 if importer_d:
2459 at_auto = importer_d.get('@auto', [])
2460 scanner_func = importer_d.get('func', None)
2461 # scanner_name = scanner_class.__name__
2462 extensions = importer_d.get('extensions', [])
2463 if at_auto:
2464 # Make entries for each @auto type.
2465 d = g.app.atAutoDict
2466 for s in at_auto:
2467 d[s] = scanner_func
2468 g.app.atAutoDict[s] = scanner_func
2469 g.app.atAutoNames.add(s)
2470 if extensions:
2471 # Make entries for each extension.
2472 d = g.app.classDispatchDict
2473 for ext in extensions:
2474 d[ext] = scanner_func #importer_d.get('func')#scanner_class
2475 elif sfn not in (
2476 # These are base classes, not real plugins.
2477 'basescanner.py',
2478 'linescanner.py',
2479 ):
2480 g.warning(f"leo/plugins/importers/{sfn} has no importer_dict")
2481 #@+node:ekr.20140728040812.17990: *6* LM.createWritersData & helper
2482 def createWritersData(self):
2483 """Create the data structures describing writer plugins."""
2484 trace = False and 'createWritersData' not in g.app.debug_dict
2485 # Do *not* remove this trace.
2486 if trace:
2487 # Suppress multiple traces.
2488 g.app.debug_dict['createWritersData'] = True
2489 g.app.writersDispatchDict = {}
2490 g.app.atAutoWritersDict = {}
2491 plugins1 = g.os_path_finalize_join(g.app.homeDir, '.leo', 'plugins')
2492 plugins2 = g.os_path_finalize_join(g.app.loadDir, '..', 'plugins')
2493 for kind, plugins in (('home', plugins1), ('leo', plugins2)):
2494 pattern = g.os_path_finalize_join(g.app.loadDir,
2495 '..', 'plugins', 'writers', '*.py')
2496 for fn in g.glob_glob(pattern):
2497 sfn = g.shortFileName(fn)
2498 if sfn != '__init__.py':
2499 try:
2500 # Important: use importlib to give imported modules their fully qualified names.
2501 m = importlib.import_module(f"leo.plugins.writers.{sfn[:-3]}")
2502 self.parse_writer_dict(sfn, m)
2503 except Exception:
2504 g.es_exception()
2505 g.warning(f"can not import leo.plugins.writers.{sfn}")
2506 if trace:
2507 g.trace('LM.writersDispatchDict')
2508 g.printDict(g.app.writersDispatchDict)
2509 g.trace('LM.atAutoWritersDict')
2510 g.printDict(g.app.atAutoWritersDict)
2511 # Creates problems: See #40.
2512 #@+node:ekr.20140728040812.17991: *7* LM.parse_writer_dict
2513 def parse_writer_dict(self, sfn, m):
2514 """
2515 Set entries in g.app.writersDispatchDict and g.app.atAutoWritersDict
2516 using entries in m.writers_dict.
2517 """
2518 writer_d = getattr(m, 'writer_dict', None)
2519 if writer_d:
2520 at_auto = writer_d.get('@auto', [])
2521 scanner_class = writer_d.get('class', None)
2522 extensions = writer_d.get('extensions', [])
2523 if at_auto:
2524 # Make entries for each @auto type.
2525 d = g.app.atAutoWritersDict
2526 for s in at_auto:
2527 aClass = d.get(s)
2528 if aClass and aClass != scanner_class:
2529 g.trace(
2530 f"{sfn}: duplicate {s} class {aClass.__name__} "
2531 f"in {m.__file__}:")
2532 else:
2533 d[s] = scanner_class
2534 g.app.atAutoNames.add(s)
2535 if extensions:
2536 # Make entries for each extension.
2537 d = g.app.writersDispatchDict
2538 for ext in extensions:
2539 aClass = d.get(ext)
2540 if aClass and aClass != scanner_class:
2541 g.trace(f"{sfn}: duplicate {ext} class", aClass, scanner_class)
2542 else:
2543 d[ext] = scanner_class
2544 elif sfn not in ('basewriter.py',):
2545 g.warning(f"leo/plugins/writers/{sfn} has no writer_dict")
2546 #@+node:ekr.20120219154958.10478: *5* LM.createGui
2547 def createGui(self, pymacs):
2548 lm = self
2549 gui_option = lm.options.get('gui')
2550 windowFlag = lm.options.get('windowFlag')
2551 script = lm.options.get('script')
2552 if g.app.gui:
2553 if g.app.gui == g.app.nullGui:
2554 g.app.gui = None # Enable g.app.createDefaultGui
2555 g.app.createDefaultGui(__file__)
2556 else:
2557 pass
2558 # This can happen when launching Leo from IPython.
2559 # This can also happen when leoID does not exist.
2560 elif gui_option is None:
2561 if script and not windowFlag:
2562 # Always use null gui for scripts.
2563 g.app.createNullGuiWithScript(script)
2564 else:
2565 g.app.createDefaultGui(__file__)
2566 else:
2567 lm.createSpecialGui(gui_option, pymacs, script, windowFlag)
2568 #@+node:ekr.20120219154958.10479: *5* LM.createSpecialGui
2569 def createSpecialGui(self, gui, pymacs, script, windowFlag):
2570 # lm = self
2571 if pymacs:
2572 g.app.createNullGuiWithScript(script=None)
2573 elif script:
2574 if windowFlag:
2575 g.app.createDefaultGui()
2576 g.app.gui.setScript(script=script)
2577 sys.argv = [] # 2021/06/24: corrected by mypy.
2578 else:
2579 g.app.createNullGuiWithScript(script=script)
2580 else:
2581 g.app.createDefaultGui()
2582 #@+node:ekr.20120219154958.10482: *5* LM.getDefaultFile
2583 def getDefaultFile(self):
2584 # Get the name of the workbook.
2585 fn = g.app.config.getString('default-leo-file')
2586 fn = g.os_path_finalize(fn)
2587 if not fn:
2588 return None
2589 if g.os_path_exists(fn):
2590 return fn
2591 if g.os_path_isabs(fn):
2592 # Create the file.
2593 g.error(f"Using default leo file name:\n{fn}")
2594 return fn
2595 # It's too risky to open a default file if it is relative.
2596 return None
2597 #@+node:ekr.20120219154958.10484: *5* LM.initApp
2598 def initApp(self, verbose):
2600 self.createAllImporterData()
2601 # Can be done early. Uses only g.app.loadDir & g.app.homeDir.
2602 assert g.app.loadManager
2603 from leo.core import leoBackground
2604 from leo.core import leoConfig
2605 from leo.core import leoNodes
2606 from leo.core import leoPlugins
2607 from leo.core import leoSessions
2608 # Import leoIPython only if requested. The import is quite slow.
2609 self.setStdStreams()
2610 if g.app.useIpython:
2611 from leo.core import leoIPython
2612 # This launches the IPython Qt Console. It *is* required.
2613 assert leoIPython # suppress pyflakes/flake8 warning.
2614 # Make sure we call the new leoPlugins.init top-level function.
2615 leoPlugins.init()
2616 # Force the user to set g.app.leoID.
2617 g.app.setLeoID(verbose=verbose)
2618 # Create early classes *after* doing plugins.init()
2619 g.app.idleTimeManager = IdleTimeManager()
2620 g.app.backgroundProcessManager = leoBackground.BackgroundProcessManager()
2621 g.app.externalFilesController = leoExternalFiles.ExternalFilesController()
2622 g.app.recentFilesManager = RecentFilesManager()
2623 g.app.config = leoConfig.GlobalConfigManager()
2624 g.app.nodeIndices = leoNodes.NodeIndices(g.app.leoID)
2625 g.app.sessionManager = leoSessions.SessionManager()
2626 # Complete the plugins class last.
2627 g.app.pluginsController.finishCreate()
2628 #@+node:ekr.20210927034148.1: *5* LM.scanOptions & helpers
2629 def scanOptions(self, fileName, pymacs):
2630 """Handle all options, remove them from sys.argv and set lm.options."""
2631 lm = self
2632 table = (
2633 '--dock',
2634 '--global-docks', # #1643. use --use-docks instead.
2635 '--init-docks',
2636 '--no-cache',
2637 '--no-dock', # #1171 and #1514: use --use-docks instead.
2638 '--session-restore',
2639 '--session-save',
2640 '--use-docks',
2641 )
2642 trace_m = textwrap.dedent("""\
2643 abbrev, beauty, cache, coloring, drawing, events, focus, git, gnx
2644 importers, ipython, keys, layouts, plugins, save, select, sections,
2645 shutdown, size, speed, startup, themes, undo, verbose, zoom
2646 """)
2647 for bad_option in table:
2648 if bad_option in sys.argv:
2649 sys.argv.remove(bad_option)
2650 print(f"Ignoring the unused/deprecated {bad_option} option")
2651 lm.old_argv = sys.argv[:]
2652 # Automatically implements the --help option.
2653 description = "usage: launchLeo.py [options] file1, file2, ..."
2654 parser = argparse.ArgumentParser(
2655 description=description,
2656 formatter_class=argparse.RawTextHelpFormatter)
2657 #
2658 # Parse the options, and remove them from sys.argv.
2659 self.addOptionsToParser(parser, trace_m)
2660 args = parser.parse_args()
2661 # Handle simple args...
2662 self.doSimpleOptions(args, trace_m)
2663 # Compute the lm.files ivar.
2664 lm.files = lm.computeFilesList(fileName)
2665 # Compute the script. Used twice below.
2666 script = None if pymacs else self.doScriptOption(args, parser)
2667 # Return the dictionary of options.
2668 return {
2669 'gui': lm.doGuiOption(args),
2670 'load_type': lm.doLoadTypeOption(args),
2671 'screenshot_fn': lm.doScreenShotOption(args), # --screen-shot=fn
2672 'script': script,
2673 'select': args.select and args.select.strip('"'),
2674 # --select=headline
2675 'theme_path': args.theme, # --theme=name
2676 'version': args.version, # --version: print the version and exit.
2677 'windowFlag': script and args.script_window,
2678 'windowSize': lm.doWindowSizeOption(args),
2679 'windowSpot': lm.doWindowSpotOption(args),
2680 }
2681 #@+node:ekr.20210927034148.2: *6* LM.addOptionsToParser
2682 #@@nobeautify
2684 def addOptionsToParser(self, parser, trace_m):
2685 """Init the argsparse parser."""
2686 add = parser.add_argument
2687 add('PATHS', nargs='*', metavar='FILES',
2688 help='list of files')
2689 add('--diff', dest='diff', action='store_true',
2690 help='use Leo as an external git diff')
2691 add('--fail-fast', dest='fail_fast', action='store_true',
2692 help='stop unit tests after the first failure')
2693 add('--fullscreen', dest='fullscreen', action='store_true',
2694 help='start fullscreen')
2695 add('--ipython', dest='ipython', action='store_true',
2696 help='enable ipython support')
2697 add('--gui', dest='gui', metavar='GUI',
2698 help='gui to use (qt/console/null)')
2699 add('--listen-to-log', dest='listen_to_log', action='store_true',
2700 help='start log_listener.py on startup')
2701 add('--load-type', dest='load_type', metavar='TYPE',
2702 help='@<file> type for non-outlines')
2703 add('--maximized', dest='maximized', action='store_true',
2704 help='start maximized')
2705 add('--minimized', dest='minimized', action='store_true',
2706 help='start minimized')
2707 add('--no-plugins', dest='no_plugins', action='store_true',
2708 help='disable all plugins')
2709 add('--no-splash', dest='no_splash', action='store_true',
2710 help='disable the splash screen')
2711 add('--quit', dest='quit', action='store_true',
2712 help='quit immediately after loading')
2713 add('--screen-shot', dest='screen_shot', metavar='PATH',
2714 help='take a screen shot and then exit')
2715 add('--script', dest='script', metavar="PATH",
2716 help='execute a script and then exit')
2717 add('--script-window', dest='script_window', action='store_true',
2718 help='execute script using default gui')
2719 add('--select', dest='select', metavar='ID',
2720 help='headline or gnx of node to select')
2721 add('--silent', dest='silent', action='store_true',
2722 help='disable all log messages')
2723 add('--theme', dest='theme', metavar='NAME',
2724 help='use the named theme file')
2725 add('--trace', dest='trace', metavar='TRACE-KEY',
2726 help=f"add one or more strings to g.app.debug. One or more of...\n{trace_m}")
2727 add('--trace-binding', dest='trace_binding', metavar='KEY',
2728 help='trace commands bound to a key')
2729 add('--trace-setting', dest='trace_setting', metavar="NAME",
2730 help='trace where named setting is set')
2731 add('--window-size', dest='window_size', metavar='SIZE',
2732 help='initial window size (height x width)')
2733 add('--window-spot', dest='window_spot', metavar='SPOT',
2734 help='initial window position (top x left)')
2735 add('-v', '--version', dest='version', action='store_true',
2736 help='print version number and exit')
2737 #@+node:ekr.20210927034148.3: *6* LM.computeFilesList
2738 def computeFilesList(self, fileName):
2739 """Return the list of files on the command line."""
2740 lm = self
2741 files = []
2742 if fileName:
2743 files.append(fileName)
2744 for arg in sys.argv[1:]:
2745 if arg and not arg.startswith('-'):
2746 files.append(arg)
2747 result = []
2748 for z in files:
2749 # Fix #245: wrong: result.extend(glob.glob(lm.completeFileName(z)))
2750 aList = g.glob_glob(lm.completeFileName(z))
2751 if aList:
2752 result.extend(aList)
2753 else:
2754 result.append(z)
2755 return [g.os_path_normslashes(z) for z in result]
2756 #@+node:ekr.20210927034148.4: *6* LM.doGuiOption
2757 def doGuiOption(self, args):
2758 gui = args.gui
2759 if gui:
2760 gui = gui.lower()
2761 if gui in ('qt', 'qttabs'):
2762 gui = 'qt' # Allow qttabs gui.
2763 elif gui.startswith('browser'):
2764 pass
2765 elif gui in ('console', 'curses', 'text', 'null'):
2766 pass
2767 else:
2768 print(f"scanOptions: unknown gui: {gui}. Using qt gui")
2769 gui = 'qt'
2770 else:
2771 gui = 'qt'
2772 assert gui
2773 g.app.guiArgName = gui
2774 return gui
2775 #@+node:ekr.20210927034148.5: *6* LM.doLoadTypeOption
2776 def doLoadTypeOption(self, args):
2778 s = args.load_type
2779 s = s.lower() if s else 'edit'
2780 return '@' + s
2781 #@+node:ekr.20210927034148.6: *6* LM.doScreenShotOption
2782 def doScreenShotOption(self, args):
2784 # --screen-shot=fn
2785 s = args.screen_shot
2786 if s:
2787 s = s.strip('"')
2788 return s
2789 #@+node:ekr.20210927034148.7: *6* LM.doScriptOption
2790 def doScriptOption(self, args, parser):
2792 # --script
2793 script = args.script
2794 if script:
2795 # #1090: use cwd, not g.app.loadDir, to find scripts.
2796 fn = g.os_path_finalize_join(os.getcwd(), script)
2797 script, e = g.readFileIntoString(fn, kind='script:', verbose=False)
2798 if not script:
2799 print(f"script not found: {fn}")
2800 sys.exit(1)
2801 else:
2802 script = None
2803 return script
2804 #@+node:ekr.20210927034148.8: *6* LM.doSimpleOptions
2805 def doSimpleOptions(self, args, trace_m):
2806 """These args just set g.app ivars."""
2807 # --fail-fast
2808 g.app.failFast = args.fail_fast
2809 # --fullscreen
2810 g.app.start_fullscreen = args.fullscreen
2811 # --git-diff
2812 g.app.diff = args.diff
2813 # --listen-to-log
2814 g.app.listen_to_log_flag = args.listen_to_log
2815 # --ipython
2816 g.app.useIpython = args.ipython
2817 # --maximized
2818 g.app.start_maximized = args.maximized
2819 # --minimized
2820 g.app.start_minimized = args.minimized
2821 # --no-plugins
2822 if args.no_plugins:
2823 g.app.enablePlugins = False
2824 # --no-splash: --minimized disables the splash screen
2825 g.app.use_splash_screen = not args.no_splash and not args.minimized
2826 # -- quit
2827 g.app.quit_after_load = args.quit
2828 # --silent
2829 g.app.silentMode = args.silent
2830 # --trace=...
2831 valid = trace_m.replace(' ', '').replace('\n', '').split(',')
2832 if args.trace:
2833 ok = True
2834 values = args.trace.lstrip('(').lstrip('[').rstrip(')').rstrip(']')
2835 for val in values.split(','):
2836 if val in valid:
2837 g.app.debug.append(val)
2838 else:
2839 g.es_print(f"unknown --trace value: {val}")
2840 ok = False
2841 if not ok:
2842 g.es_print('Valid --trace values are...')
2843 for line in trace_m.split('\n'):
2844 print(' ', line.rstrip())
2845 #
2846 # These are not bool args.
2847 # --trace-binding
2848 g.app.trace_binding = args.trace_binding
2849 # g.app.config does not exist yet.
2850 #
2851 # --trace-setting=setting
2852 g.app.trace_setting = args.trace_setting
2853 # g.app.config does not exist yet.
2854 #@+node:ekr.20210927034148.9: *6* LM.doWindowSpotOption
2855 def doWindowSpotOption(self, args):
2857 # --window-spot
2858 spot = args.window_spot
2859 if spot:
2860 try:
2861 top, left = spot.split('x')
2862 spot = int(top), int(left)
2863 except ValueError:
2864 print('scanOptions: bad --window-spot:', spot)
2865 spot = None
2867 return spot
2868 #@+node:ekr.20210927034148.10: *6* LM.doWindowSizeOption
2869 def doWindowSizeOption(self, args):
2871 # --window-size
2872 windowSize = args.window_size
2873 if windowSize:
2874 try:
2875 h, w = windowSize.split('x')
2876 windowSize = int(h), int(w)
2877 except ValueError:
2878 windowSize = None
2879 print('scanOptions: bad --window-size:', windowSize)
2880 return windowSize
2881 #@+node:ekr.20160718072648.1: *5* LM.setStdStreams
2882 def setStdStreams(self):
2883 """
2884 Make sure that stdout and stderr exist.
2885 This is an issue when running Leo with pythonw.exe.
2886 """
2887 # Define class LeoStdOut
2888 #@+others
2889 #@+node:ekr.20160718091844.1: *6* class LeoStdOut
2890 class LeoStdOut:
2891 """A class to put stderr & stdout to Leo's log pane."""
2893 def __init__(self, kind):
2894 self.kind = kind
2895 g.es_print = self.write
2896 g.pr = self.write
2898 def flush(self, *args, **keys):
2899 pass
2900 #@+others
2901 #@+node:ekr.20160718102306.1: *7* LeoStdOut.write
2902 def write(self, *args, **keys):
2903 """Put all non-keyword args to the log pane, as in g.es."""
2904 #
2905 # Tracing will lead to unbounded recursion unless
2906 # sys.stderr has been redirected on the command line.
2907 app = g.app
2908 if not app or app.killed:
2909 return
2910 if app.gui and app.gui.consoleOnly:
2911 return
2912 log = app.log
2913 # Compute the effective args.
2914 d = {
2915 'color': None,
2916 'commas': False,
2917 'newline': True,
2918 'spaces': True,
2919 'tabName': 'Log',
2920 }
2921 # Handle keywords for g.pr and g.es_print.
2922 d = g.doKeywordArgs(keys, d)
2923 color: Any = d.get('color')
2924 if color == 'suppress':
2925 return
2926 if log and color is None:
2927 color = g.actualColor('black')
2928 color = g.actualColor(color)
2929 tabName = d.get('tabName') or 'Log'
2930 newline = d.get('newline')
2931 s = g.translateArgs(args, d)
2932 if app.batchMode:
2933 if log:
2934 log.put(s)
2935 elif log and app.logInited:
2936 # from_redirect is the big difference between this and g.es.
2937 log.put(s, color=color, tabName=tabName, from_redirect=True)
2938 else:
2939 app.logWaiting.append((s, color, newline),)
2940 #@-others
2941 #@-others
2942 if not sys.stdout:
2943 sys.stdout = sys.__stdout__ = LeoStdOut('stdout') # type:ignore
2944 if not sys.stderr:
2945 sys.stderr = sys.__stderr__ = LeoStdOut('stderr') # type:ignore
2946 #@+node:ekr.20120219154958.10491: *4* LM.isValidPython
2947 def isValidPython(self):
2948 if sys.platform == 'cli':
2949 return True
2950 message = (
2951 f"Leo requires Python {g.minimum_python_version} or higher"
2952 f"You may download Python from http://python.org/download/")
2953 try:
2954 version = '.'.join([str(sys.version_info[i]) for i in (0, 1, 2)])
2955 ok = g.CheckVersion(version, g.minimum_python_version)
2956 if not ok:
2957 print(message)
2958 try:
2959 # g.app.gui does not exist yet.
2960 d = g.EmergencyDialog(
2961 title='Python Version Error',
2962 message=message)
2963 d.run()
2964 except Exception:
2965 g.es_exception()
2966 return ok
2967 except Exception:
2968 print("isValidPython: unexpected exception: g.CheckVersion")
2969 traceback.print_exc()
2970 return 0
2971 #@+node:ekr.20120223062418.10393: *4* LM.loadLocalFile & helper
2972 def loadLocalFile(self, fn, gui, old_c):
2973 """Completely read a file, creating the corresonding outline.
2975 1. If fn is an existing .leo, .db or .leojs file, read it twice:
2976 the first time with a NullGui to discover settings,
2977 the second time with the requested gui to create the outline.
2979 2. If fn is an external file:
2980 get settings from the leoSettings.leo and myLeoSetting.leo, then
2981 create a "wrapper" outline continain an @file node for the external file.
2983 3. If fn is empty:
2984 get settings from the leoSettings.leo and myLeoSetting.leo or default settings,
2985 or open an empty outline.
2986 """
2987 lm = self
2988 # Step 1: Return if the file is already open.
2989 fn = g.os_path_finalize(fn)
2990 if fn:
2991 c = lm.findOpenFile(fn)
2992 if c:
2993 return c
2994 #
2995 # Step 2: get the previous settings.
2996 # For .leo files (and zipped .leo files) this pre-reads the file in a null gui.
2997 # Otherwise, get settings from leoSettings.leo, myLeoSettings.leo, or default settings.
2998 previousSettings = lm.getPreviousSettings(fn)
2999 #
3000 # Step 3: open the outline in the requested gui.
3001 # For .leo files (and zipped .leo file) this opens the file a second time.
3002 c = lm.openFileByName(fn, gui, old_c, previousSettings)
3003 return c
3004 #@+node:ekr.20120223062418.10394: *5* LM.openFileByName & helpers
3005 def openFileByName(self, fn, gui, old_c, previousSettings):
3006 """Read the local file whose full path is fn using the given gui.
3007 fn may be a Leo file (including .leo or zipped file) or an external file.
3009 This is not a pre-read: the previousSettings always exist and
3010 the commander created here persists until the user closes the outline.
3012 Reads the entire outline if fn exists and is a .leo file or zipped file.
3013 Creates an empty outline if fn is a non-existent Leo file.
3014 Creates an wrapper outline if fn is an external file, existing or not.
3015 """
3016 lm = self
3017 # Disable the log.
3018 g.app.setLog(None)
3019 g.app.lockLog()
3020 # Create the a commander for the .leo file.
3021 # Important. The settings don't matter for pre-reads!
3022 # For second read, the settings for the file are *exactly* previousSettings.
3023 c = g.app.newCommander(fileName=fn, gui=gui, previousSettings=previousSettings)
3024 # Open the file, if possible.
3025 g.doHook('open0')
3026 theFile = lm.openAnyLeoFile(fn)
3027 if isinstance(theFile, sqlite3.Connection):
3028 # this commander is associated with sqlite db
3029 c.sqlite_connection = theFile
3030 # Enable the log.
3031 g.app.unlockLog()
3032 c.frame.log.enable(True)
3033 # Phase 2: Create the outline.
3034 g.doHook("open1", old_c=None, c=c, new_c=c, fileName=fn)
3035 if theFile:
3036 readAtFileNodesFlag = bool(previousSettings)
3037 # The log is not set properly here.
3038 ok = lm.readOpenedLeoFile(c, fn, readAtFileNodesFlag, theFile)
3039 # Call c.fileCommands.openLeoFile to read the .leo file.
3040 if not ok:
3041 return None
3042 else:
3043 # Create a wrapper .leo file if:
3044 # a) fn is a .leo file that does not exist or
3045 # b) fn is an external file, existing or not.
3046 lm.initWrapperLeoFile(c, fn)
3047 g.doHook("open2", old_c=None, c=c, new_c=c, fileName=fn)
3048 # Phase 3: Complete the initialization.
3049 g.app.writeWaitingLog(c)
3050 c.setLog()
3051 lm.createMenu(c, fn)
3052 lm.finishOpen(c)
3053 return c
3054 #@+node:ekr.20120223062418.10405: *6* LM.createMenu
3055 def createMenu(self, c, fn=None):
3056 # lm = self
3057 # Create the menu as late as possible so it can use user commands.
3058 if not g.doHook("menu1", c=c, p=c.p, v=c.p):
3059 c.frame.menu.createMenuBar(c.frame)
3060 g.app.recentFilesManager.updateRecentFiles(fn)
3061 g.doHook("menu2", c=c, p=c.p, v=c.p)
3062 g.doHook("after-create-leo-frame", c=c)
3063 g.doHook("after-create-leo-frame2", c=c)
3064 # Fix bug 844953: tell Unity which menu to use.
3065 # c.enableMenuBar()
3066 #@+node:ekr.20120223062418.10406: *6* LM.findOpenFile
3067 def findOpenFile(self, fn):
3069 def munge(name):
3070 return g.os_path_normpath(name or '').lower()
3072 for frame in g.app.windowList:
3073 c = frame.c
3074 if g.os_path_realpath(munge(fn)) == g.os_path_realpath(munge(c.mFileName)):
3075 # Don't call frame.bringToFront(), it breaks --minimize
3076 c.setLog()
3077 # Selecting the new tab ensures focus is set.
3078 master = getattr(frame.top, 'leo_master', None)
3079 if master: # master is a TabbedTopLevel.
3080 master.select(frame.c)
3081 c.outerUpdate()
3082 return c
3083 return None
3084 #@+node:ekr.20120223062418.10407: *6* LM.finishOpen
3085 def finishOpen(self, c):
3086 # lm = self
3087 k = c.k
3088 assert k
3089 # New in Leo 4.6: provide an official way for very late initialization.
3090 c.frame.tree.initAfterLoad()
3091 c.initAfterLoad()
3092 # chapterController.finishCreate must be called after the first real redraw
3093 # because it requires a valid value for c.rootPosition().
3094 if c.chapterController:
3095 c.chapterController.finishCreate()
3096 if k:
3097 k.setDefaultInputState()
3098 c.initialFocusHelper()
3099 if k:
3100 k.showStateAndMode()
3101 c.frame.initCompleteHint()
3102 c.outerUpdate()
3103 # #181: Honor focus requests.
3104 #@+node:ekr.20120223062418.10408: *6* LM.initWrapperLeoFile
3105 def initWrapperLeoFile(self, c, fn):
3106 """
3107 Create an empty file if the external fn is empty.
3109 Otherwise, create an @edit or @file node for the external file.
3110 """
3111 # lm = self
3112 # Use the config params to set the size and location of the window.
3113 frame = c.frame
3114 frame.setInitialWindowGeometry()
3115 frame.deiconify()
3116 frame.lift()
3117 # #1570: Resize the _new_ frame.
3118 frame.splitVerticalFlag, r1, r2 = frame.initialRatios()
3119 frame.resizePanesToRatio(r1, r2)
3120 if not g.os_path_exists(fn):
3121 p = c.rootPosition()
3122 # Create an empty @edit node unless fn is an .leo file.
3123 # Fix #1070: Use "newHeadline", not fn.
3124 p.h = "newHeadline" if fn.endswith('.leo') else f"@edit {fn}"
3125 c.selectPosition(p)
3126 elif c.looksLikeDerivedFile(fn):
3127 # 2011/10/10: Create an @file node.
3128 p = c.importCommands.importDerivedFiles(parent=c.rootPosition(),
3129 paths=[fn], command=None) # Not undoable.
3130 if p and p.hasBack():
3131 p.back().doDelete()
3132 p = c.rootPosition()
3133 if not p:
3134 return None
3135 else:
3136 # Create an @<file> node.
3137 p = c.rootPosition()
3138 if p:
3139 load_type = self.options['load_type']
3140 p.setHeadString(f"{load_type} {fn}")
3141 c.refreshFromDisk()
3142 c.selectPosition(p)
3144 # Fix critical bug 1184855: data loss with command line 'leo somefile.ext'
3145 # Fix smallish bug 1226816 Command line "leo xxx.leo" creates file xxx.leo.leo.
3146 c.mFileName = fn if fn.endswith('.leo') else f"{fn}.leo"
3147 c.wrappedFileName = fn
3148 c.frame.title = c.computeWindowTitle(c.mFileName)
3149 c.frame.setTitle(c.frame.title)
3150 # chapterController.finishCreate must be called after the first real redraw
3151 # because it requires a valid value for c.rootPosition().
3152 if c.config.getBool('use-chapters') and c.chapterController:
3153 c.chapterController.finishCreate()
3154 frame.c.clearChanged()
3155 # Mark the outline clean.
3156 # This makes it easy to open non-Leo files for quick study.
3157 return c
3158 #@+node:ekr.20120223062418.10419: *6* LM.isLeoFile & LM.isZippedFile
3159 def isLeoFile(self, fn):
3160 if not fn:
3161 return False
3162 return zipfile.is_zipfile(fn) or fn.endswith(('.leo', 'db', '.leojs'))
3164 def isZippedFile(self, fn):
3165 return fn and zipfile.is_zipfile(fn)
3166 #@+node:ekr.20120224161905.10030: *6* LM.openAnyLeoFile
3167 def openAnyLeoFile(self, fn):
3168 """Open a .leo, .leojs or .db file."""
3169 lm = self
3170 if fn.endswith('.db'):
3171 return sqlite3.connect(fn)
3172 if lm.isLeoFile(fn) and g.os_path_exists(fn):
3173 if lm.isZippedFile(fn):
3174 theFile = lm.openZipFile(fn)
3175 else:
3176 theFile = lm.openLeoFile(fn)
3177 else:
3178 theFile = None
3179 return theFile
3180 #@+node:ekr.20120223062418.10416: *6* LM.openLeoFile
3181 def openLeoFile(self, fn):
3182 # lm = self
3183 try:
3184 theFile = open(fn, 'rb')
3185 return theFile
3186 except IOError:
3187 # Do not use string + here: it will fail for non-ascii strings!
3188 if not g.unitTesting:
3189 g.error("can not open:", fn)
3190 return None
3191 #@+node:ekr.20120223062418.10410: *6* LM.openZipFile
3192 def openZipFile(self, fn):
3193 # lm = self
3194 try:
3195 theFile = zipfile.ZipFile(fn, 'r')
3196 if not theFile:
3197 return None
3198 # Read the file into an StringIO file.
3199 aList = theFile.namelist()
3200 name = aList and len(aList) == 1 and aList[0]
3201 if not name:
3202 return None
3203 s = theFile.read(name)
3204 s2 = g.toUnicode(s, 'utf-8')
3205 return StringIO(s2)
3206 except IOError:
3207 # Do not use string + here: it will fail for non-ascii strings!
3208 if not g.unitTesting:
3209 g.error("can not open:", fn)
3210 return None
3211 #@+node:ekr.20120223062418.10412: *6* LM.readOpenedLeoFile
3212 def readOpenedLeoFile(self, c, fn, readAtFileNodesFlag, theFile):
3213 # New in Leo 4.10: The open1 event does not allow an override of the init logic.
3214 assert theFile
3215 # lm = self
3216 ok = c.fileCommands.openLeoFile(theFile, fn,
3217 readAtFileNodesFlag=readAtFileNodesFlag)
3218 # closes file.
3219 if ok:
3220 if not c.openDirectory:
3221 theDir = g.os_path_finalize(g.os_path_dirname(fn)) # 1341
3222 c.openDirectory = c.frame.openDirectory = theDir
3223 else:
3224 g.app.closeLeoWindow(c.frame, finish_quit=False)
3225 # #970: Never close Leo here.
3226 return ok
3227 #@+node:ekr.20160430063406.1: *3* LM.revertCommander
3228 def revertCommander(self, c):
3229 """Revert c to the previously saved contents."""
3230 lm = self
3231 fn = c.mFileName
3232 # Re-read the file.
3233 theFile = lm.openAnyLeoFile(fn)
3234 if theFile:
3235 c.fileCommands.initIvars()
3236 c.fileCommands.getLeoFile(theFile, fn, checkOpenFiles=False)
3237 # Closes the file.
3238 #@-others
3239#@+node:ekr.20120223062418.10420: ** class PreviousSettings
3240class PreviousSettings:
3241 """
3242 A class holding the settings and shortcuts dictionaries
3243 that are computed in the first pass when loading local
3244 files and passed to the second pass.
3245 """
3247 def __init__(self, settingsDict, shortcutsDict):
3248 if not shortcutsDict or not settingsDict: # #1766: unit tests.
3249 lm = g.app.loadManager
3250 settingsDict, shortcutsDict = lm.createDefaultSettingsDicts()
3251 self.settingsDict = settingsDict
3252 self.shortcutsDict = shortcutsDict
3254 def __repr__(self):
3255 return (
3256 f"<PreviousSettings\n"
3257 f"{self.settingsDict}\n"
3258 f"{self.shortcutsDict}\n>")
3260 __str__ = __repr__
3261#@+node:ekr.20120225072226.10283: ** class RecentFilesManager
3262class RecentFilesManager:
3263 """A class to manipulate leoRecentFiles.txt."""
3265 def __init__(self):
3267 self.edit_headline = 'Recent files. Do not change this headline!'
3268 # Headline used by
3269 self.groupedMenus = []
3270 # Set in rf.createRecentFilesMenuItems.
3271 self.recentFiles = []
3272 # List of g.Bunches describing .leoRecentFiles.txt files.
3273 self.recentFilesMenuName = 'Recent Files'
3274 # May be changed later.
3275 self.recentFileMessageWritten = False
3276 # To suppress all but the first message.
3277 self.write_recent_files_as_needed = False
3278 # Will be set later.
3279 #@+others
3280 #@+node:ekr.20041201080436: *3* rf.appendToRecentFiles
3281 def appendToRecentFiles(self, files):
3282 rf = self
3283 files = [theFile.strip() for theFile in files]
3285 def munge(name):
3286 return g.os_path_normpath(name or '').lower()
3288 for name in files:
3289 # Remove all variants of name.
3290 for name2 in rf.recentFiles[:]:
3291 if munge(name) == munge(name2):
3292 rf.recentFiles.remove(name2)
3293 rf.recentFiles.append(name)
3294 #@+node:ekr.20120225072226.10289: *3* rf.cleanRecentFiles
3295 def cleanRecentFiles(self, c):
3296 """
3297 Remove items from the recent files list that no longer exist.
3299 This almost never does anything because Leo's startup logic removes
3300 nonexistent files from the recent files list.
3301 """
3302 result = [z for z in self.recentFiles if g.os_path_exists(z)]
3303 if result != self.recentFiles:
3304 for path in result:
3305 self.updateRecentFiles(path)
3306 self.writeRecentFilesFile(c)
3307 #@+node:ekr.20180212141017.1: *3* rf.demangleRecentFiles
3308 def demangleRecentFiles(self, c, data):
3309 """Rewrite recent files based on c.config.getData('path-demangle')"""
3310 changes = []
3311 replace = None
3312 for line in data:
3313 text = line.strip()
3314 if text.startswith('REPLACE: '):
3315 replace = text.split(None, 1)[1].strip()
3316 if text.startswith('WITH:') and replace is not None:
3317 with_ = text[5:].strip()
3318 changes.append((replace, with_))
3319 g.es(f"{replace} -> {with_}")
3320 orig = [z for z in self.recentFiles if z.startswith("/")]
3321 self.recentFiles = []
3322 for i in orig:
3323 t = i
3324 for change in changes:
3325 t = t.replace(*change)
3326 self.updateRecentFiles(t)
3327 self.writeRecentFilesFile(c)
3328 #@+node:ekr.20120225072226.10297: *3* rf.clearRecentFiles
3329 def clearRecentFiles(self, c):
3330 """Clear the recent files list, then add the present file."""
3331 rf = self
3332 menu, u = c.frame.menu, c.undoer
3333 bunch = u.beforeClearRecentFiles()
3334 recentFilesMenu = menu.getMenu(self.recentFilesMenuName)
3335 menu.deleteRecentFilesMenuItems(recentFilesMenu)
3336 rf.recentFiles = [c.fileName()]
3337 for frame in g.app.windowList:
3338 rf.createRecentFilesMenuItems(frame.c)
3339 u.afterClearRecentFiles(bunch)
3340 # Write the file immediately.
3341 rf.writeRecentFilesFile(c)
3342 #@+node:ekr.20120225072226.10301: *3* rf.createRecentFilesMenuItems
3343 def createRecentFilesMenuItems(self, c):
3344 rf = self
3345 menu = c.frame.menu
3346 recentFilesMenu = menu.getMenu(self.recentFilesMenuName)
3347 if not recentFilesMenu:
3348 return
3349 # Delete all previous entries.
3350 menu.deleteRecentFilesMenuItems(recentFilesMenu)
3351 # Create the permanent (static) menu entries.
3352 table = rf.getRecentFilesTable()
3353 menu.createMenuEntries(recentFilesMenu, table)
3354 # Create all the other entries (a maximum of 36).
3355 accel_ch = string.digits + string.ascii_uppercase # Not a unicode problem.
3356 i = 0
3357 n = len(accel_ch)
3358 # see if we're grouping when files occur in more than one place
3359 rf_group = c.config.getBool("recent-files-group")
3360 rf_always = c.config.getBool("recent-files-group-always")
3361 groupedEntries = rf_group or rf_always
3362 if groupedEntries: # if so, make dict of groups
3363 dirCount: Dict[str, Any] = {}
3364 for fileName in rf.getRecentFiles()[:n]:
3365 dirName, baseName = g.os_path_split(fileName)
3366 if baseName not in dirCount:
3367 dirCount[baseName] = {'dirs': [], 'entry': None}
3368 dirCount[baseName]['dirs'].append(dirName)
3369 for name in rf.getRecentFiles()[:n]:
3370 # pylint: disable=cell-var-from-loop
3371 if name.strip() == "":
3372 continue # happens with empty list/new file
3374 def recentFilesCallback(event=None, c=c, name=name):
3375 c.openRecentFile(fn=name)
3377 if groupedEntries:
3378 dirName, baseName = g.os_path_split(name)
3379 entry = dirCount[baseName]
3380 if len(entry['dirs']) > 1 or rf_always: # sub menus
3381 if entry['entry'] is None:
3382 entry['entry'] = menu.createNewMenu(baseName, "Recent Files...")
3383 # acts as a flag for the need to create the menu
3384 c.add_command(menu.getMenu(baseName), label=dirName,
3385 command=recentFilesCallback, underline=0)
3386 else: # single occurence, no submenu
3387 c.add_command(recentFilesMenu, label=baseName,
3388 command=recentFilesCallback, underline=0)
3389 else: # original behavior
3390 label = f"{accel_ch[i]} {g.computeWindowTitle(name)}"
3391 c.add_command(recentFilesMenu, label=label,
3392 command=recentFilesCallback, underline=0)
3393 i += 1
3394 if groupedEntries: # store so we can delete them later
3395 rf.groupedMenus = [z for z in dirCount
3396 if dirCount[z]['entry'] is not None]
3397 #@+node:vitalije.20170703115609.1: *3* rf.editRecentFiles
3398 def editRecentFiles(self, c):
3399 """
3400 Dump recentFiles into new node appended as lastTopLevel, selects it and
3401 request focus in body.
3403 NOTE: command write-edited-recent-files assume that headline of this
3404 node is not changed by user.
3405 """
3406 rf = self
3407 p1 = c.lastTopLevel().insertAfter()
3408 p1.h = self.edit_headline
3409 p1.b = '\n'.join(rf.recentFiles)
3410 c.redraw()
3411 c.selectPosition(p1)
3412 c.redraw()
3413 c.bodyWantsFocusNow()
3414 g.es('edit list and run write-rff to save recentFiles')
3415 #@+node:ekr.20120225072226.10286: *3* rf.getRecentFiles
3416 def getRecentFiles(self):
3417 # Fix #299: Leo loads a deleted file.
3418 self.recentFiles = [z for z in self.recentFiles
3419 if g.os_path_exists(z)]
3420 return self.recentFiles
3421 #@+node:ekr.20120225072226.10304: *3* rf.getRecentFilesTable
3422 def getRecentFilesTable(self):
3423 return (
3424 "*clear-recent-files",
3425 "*clean-recent-files",
3426 "*demangle-recent-files",
3427 "*sort-recent-files",
3428 ("-", None, None),
3429 )
3430 #@+node:ekr.20070224115832: *3* rf.readRecentFiles & helpers
3431 def readRecentFiles(self, localConfigFile):
3432 """Read all .leoRecentFiles.txt files."""
3433 # The order of files in this list affects the order of the recent files list.
3434 rf = self
3435 seen = []
3436 localConfigPath = g.os_path_dirname(localConfigFile)
3437 for path in (g.app.homeLeoDir, g.app.globalConfigDir, localConfigPath):
3438 if path:
3439 path = g.os_path_realpath(g.os_path_finalize(path))
3440 if path and path not in seen:
3441 ok = rf.readRecentFilesFile(path)
3442 if ok:
3443 seen.append(path)
3444 if not seen and rf.write_recent_files_as_needed:
3445 rf.createRecentFiles()
3446 #@+node:ekr.20061010121944: *4* rf.createRecentFiles
3447 def createRecentFiles(self):
3448 """
3449 Try to create .leoRecentFiles.txt, in the users home directory, or in
3450 Leo's config directory if that fails.
3451 """
3452 for theDir in (g.app.homeLeoDir, g.app.globalConfigDir):
3453 if theDir:
3454 fn = g.os_path_join(theDir, '.leoRecentFiles.txt')
3455 try:
3456 with open(fn, 'w'):
3457 g.red('created', fn)
3458 return
3459 except IOError:
3460 g.error('can not create', fn)
3461 g.es_exception()
3462 #@+node:ekr.20050424115658: *4* rf.readRecentFilesFile
3463 def readRecentFilesFile(self, path):
3465 fileName = g.os_path_join(path, '.leoRecentFiles.txt')
3466 if not g.os_path_exists(fileName):
3467 return False
3468 try:
3469 with io.open(fileName, encoding='utf-8', mode='r') as f:
3470 try: # Fix #471.
3471 lines = f.readlines()
3472 except Exception:
3473 lines = None
3474 except IOError:
3475 # The file exists, so FileNotFoundError is not possible.
3476 g.trace('can not open', fileName)
3477 return False
3478 if lines and self.sanitize(lines[0]) == 'readonly':
3479 lines = lines[1:]
3480 if lines:
3481 lines = [g.toUnicode(g.os_path_normpath(line)) for line in lines]
3482 self.appendToRecentFiles(lines)
3483 return True
3484 #@+node:ekr.20120225072226.10285: *3* rf.sanitize
3485 def sanitize(self, name):
3486 """Return a sanitized file name."""
3487 if name is None:
3488 return None
3489 name = name.lower()
3490 for ch in ('-', '_', ' ', '\n'):
3491 name = name.replace(ch, '')
3492 return name or None
3493 #@+node:ekr.20120215072959.12478: *3* rf.setRecentFiles
3494 def setRecentFiles(self, files):
3495 """Update the recent files list."""
3496 rf = self
3497 rf.appendToRecentFiles(files)
3498 #@+node:ekr.20120225072226.10293: *3* rf.sortRecentFiles
3499 def sortRecentFiles(self, c):
3500 """Sort the recent files list."""
3501 rf = self
3503 def key(path):
3504 # Sort only the base name. That's what will appear in the menu.
3505 s = g.os_path_basename(path)
3506 return s.lower() if sys.platform.lower().startswith('win') else s
3508 aList = sorted(rf.recentFiles, key=key)
3509 rf.recentFiles = []
3510 for z in reversed(aList):
3511 rf.updateRecentFiles(z)
3512 rf.writeRecentFilesFile(c)
3513 #@+node:ekr.20031218072017.2083: *3* rf.updateRecentFiles
3514 def updateRecentFiles(self, fileName):
3515 """Create the RecentFiles menu. May be called with Null fileName."""
3516 rf = self
3517 if g.unitTesting:
3518 return
3520 def munge(name):
3521 return g.os_path_finalize(name or '').lower()
3523 def munge2(name):
3524 return g.os_path_finalize_join(g.app.loadDir, name or '')
3526 # Update the recent files list in all windows.
3528 if fileName:
3529 for frame in g.app.windowList:
3530 # Remove all versions of the file name.
3531 for name in rf.recentFiles:
3532 if (
3533 munge(fileName) == munge(name) or
3534 munge2(fileName) == munge2(name)
3535 ):
3536 rf.recentFiles.remove(name)
3537 rf.recentFiles.insert(0, fileName)
3538 # Recreate the Recent Files menu.
3539 rf.createRecentFilesMenuItems(frame.c)
3540 else:
3541 for frame in g.app.windowList:
3542 rf.createRecentFilesMenuItems(frame.c)
3543 #@+node:vitalije.20170703115616.1: *3* rf.writeEditedRecentFiles
3544 def writeEditedRecentFiles(self, c):
3545 """
3546 Write content of "edit_headline" node as recentFiles and recreates
3547 menues.
3548 """
3549 p, rf = c.p, self
3550 p = g.findNodeAnywhere(c, self.edit_headline)
3551 if p:
3552 files = [z for z in p.b.splitlines() if z and g.os_path_exists(z)]
3553 rf.recentFiles = files
3554 rf.writeRecentFilesFile(c)
3555 rf.updateRecentFiles(None)
3556 c.selectPosition(p)
3557 c.deleteOutline()
3558 else:
3559 g.red('not found:', self.edit_headline)
3560 #@+node:ekr.20050424114937.2: *3* rf.writeRecentFilesFile & helper
3561 def writeRecentFilesFile(self, c):
3562 """Write the appropriate .leoRecentFiles.txt file."""
3563 tag = '.leoRecentFiles.txt'
3564 rf = self
3565 # tag:#661. Do nothing if in leoBride.
3566 if g.unitTesting or g.app.inBridge:
3567 return
3568 localFileName = c.fileName()
3569 if localFileName:
3570 localPath, junk = g.os_path_split(localFileName)
3571 else:
3572 localPath = None
3573 written = False
3574 seen = []
3575 for path in (localPath, g.app.globalConfigDir, g.app.homeLeoDir):
3576 if path:
3577 fileName = g.os_path_join(path, tag)
3578 if g.os_path_exists(fileName) and fileName.lower() not in seen:
3579 seen.append(fileName.lower())
3580 ok = rf.writeRecentFilesFileHelper(fileName)
3581 if ok:
3582 written = True
3583 if not rf.recentFileMessageWritten and not g.unitTesting and not g.app.silentMode: # #459:
3584 if ok:
3585 g.es_print(f"wrote recent file: {fileName}")
3586 else:
3587 g.error(f"failed to write recent file: {fileName}")
3588 if written:
3589 rf.recentFileMessageWritten = True
3590 else:
3591 # Attempt to create .leoRecentFiles.txt in the user's home directory.
3592 if g.app.homeLeoDir:
3593 fileName = g.os_path_finalize_join(g.app.homeLeoDir, tag)
3594 if not g.os_path_exists(fileName):
3595 g.red(f"creating: {fileName}")
3596 rf.writeRecentFilesFileHelper(fileName)
3597 #@+node:ekr.20050424131051: *4* rf.writeRecentFilesFileHelper
3598 def writeRecentFilesFileHelper(self, fileName):
3599 # Don't update the file if it begins with read-only.
3600 #
3601 # Part 1: Return False if the first line is "readonly".
3602 # It's ok if the file doesn't exist.
3603 if g.os_path_exists(fileName):
3604 with io.open(fileName, encoding='utf-8', mode='r') as f:
3605 try:
3606 # Fix #471.
3607 lines = f.readlines()
3608 except Exception:
3609 lines = None
3610 if lines and self.sanitize(lines[0]) == 'readonly':
3611 return False
3612 # Part 2: write the files.
3613 try:
3614 with io.open(fileName, encoding='utf-8', mode='w') as f:
3615 s = '\n'.join(self.recentFiles) if self.recentFiles else '\n'
3616 f.write(g.toUnicode(s))
3617 return True
3618 except IOError:
3619 g.error('error writing', fileName)
3620 g.es_exception()
3621 except Exception:
3622 g.error('unexpected exception writing', fileName)
3623 g.es_exception()
3624 if g.unitTesting:
3625 raise
3626 return False
3627 #@-others
3628#@+node:ekr.20150514125218.1: ** Top-level-commands
3629#@+node:ekr.20150514125218.2: *3* ctrl-click-at-cursor
3630@g.command('ctrl-click-at-cursor')
3631def ctrlClickAtCursor(event):
3632 """Simulate a control-click at the cursor."""
3633 c = event.get('c')
3634 if c:
3635 g.openUrlOnClick(event)
3636#@+node:ekr.20180213045148.1: *3* demangle-recent-files
3637@g.command('demangle-recent-files')
3638def demangle_recent_files_command(event):
3639 """
3640 Path demangling potentially alters the paths in the recent files list
3641 according to find/replace patterns in the @data path-demangle setting.
3642 For example:
3644 REPLACE: .gnome-desktop
3645 WITH: My Desktop
3647 The default setting specifies no patterns.
3648 """
3649 c = event and event.get('c')
3650 if c:
3651 data = c.config.getData('path-demangle')
3652 if data:
3653 g.app.recentFilesManager.demangleRecentFiles(c, data)
3654 else:
3655 g.es_print('No patterns in @data path-demangle')
3656#@+node:ekr.20150514125218.3: *3* enable/disable/toggle-idle-time-events
3657@g.command('disable-idle-time-events')
3658def disable_idle_time_events(event):
3659 """Disable default idle-time event handling."""
3660 g.app.idle_time_hooks_enabled = False
3662@g.command('enable-idle-time-events')
3663def enable_idle_time_events(event):
3664 """Enable default idle-time event handling."""
3665 g.app.idle_time_hooks_enabled = True
3667@g.command('toggle-idle-time-events')
3668def toggle_idle_time_events(event):
3669 """Toggle default idle-time event handling."""
3670 g.app.idle_time_hooks_enabled = not g.app.idle_time_hooks_enabled
3671#@+node:ekr.20150514125218.4: *3* join-leo-irc
3672@g.command('join-leo-irc')
3673def join_leo_irc(event=None):
3674 """Open the web page to Leo's irc channel on freenode.net."""
3675 import webbrowser
3676 webbrowser.open("http://webchat.freenode.net/?channels=%23leo&uio=d4")
3677#@+node:ekr.20150514125218.5: *3* open-url
3678@g.command('open-url')
3679def openUrl(event=None):
3680 """
3681 Open the url in the headline or body text of the selected node.
3683 Use the headline if it contains a valid url.
3684 Otherwise, look *only* at the first line of the body.
3685 """
3686 c = event.get('c')
3687 if c:
3688 g.openUrl(c.p)
3689#@+node:ekr.20150514125218.6: *3* open-url-under-cursor
3690@g.command('open-url-under-cursor')
3691def openUrlUnderCursor(event=None):
3692 """Open the url under the cursor."""
3693 return g.openUrlOnClick(event)
3694#@-others
3695#@@language python
3696#@@tabwidth -4
3697#@@pagewidth 70
3698#@-leo