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