Coverage for C:\leo.repo\leo-editor\leo\core\leoApp.py: 39%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# -*- coding: utf-8 -*-
2#@+leo-ver=5-thin
3#@+node:ekr.20031218072017.2608: * @file leoApp.py
4#@@first
5#@+<< imports >>
6#@+node:ekr.20120219194520.10463: ** << imports >> (leoApp)
7import argparse
8import importlib
9import io
10import os
11import sqlite3
12import subprocess
13import string
14import sys
15import textwrap
16import time
17import traceback
18from typing import Any, Dict
19import zipfile
20import platform
21from leo.core import leoGlobals as g
22from leo.core import leoExternalFiles
23from leo.core.leoQt import QCloseEvent
24StringIO = io.StringIO
25#@-<< imports >>
26#@+others
27#@+node:ekr.20150509193629.1: ** cmd (decorator)
28def cmd(name):
29 """Command decorator for the LeoApp class."""
30 return g.new_cmd_decorator(name, ['g', 'app'])
31#@+node:ekr.20161026122804.1: ** class IdleTimeManager
32class IdleTimeManager:
33 """
34 A singleton class to manage idle-time handling. This class handles all
35 details of running code at idle time, including running 'idle' hooks.
37 Any code can call g.app.idleTimeManager.add_callback(callback) to cause
38 the callback to be called at idle time forever.
39 """
41 def __init__(self):
42 """Ctor for IdleTimeManager class."""
43 self.callback_list = []
44 self.timer = None
45 #@+others
46 #@+node:ekr.20161026125611.1: *3* itm.add_callback
47 def add_callback(self, callback):
48 """Add a callback to be called at every idle time."""
49 self.callback_list.append(callback)
50 #@+node:ekr.20161026124810.1: *3* itm.on_idle
51 on_idle_count = 0
53 def on_idle(self, timer):
54 """IdleTimeManager: Run all idle-time callbacks."""
55 if not g.app:
56 return
57 if g.app.killed:
58 return
59 if not g.app.pluginsController:
60 g.trace('No g.app.pluginsController', g.callers())
61 timer.stop()
62 return # For debugger.
63 self.on_idle_count += 1
64 # Handle the registered callbacks.
65 for callback in self.callback_list:
66 try:
67 callback()
68 except Exception:
69 g.es_exception()
70 g.es_print(f"removing callback: {callback}")
71 self.callback_list.remove(callback)
72 # Handle idle-time hooks.
73 g.app.pluginsController.on_idle()
74 #@+node:ekr.20161028034808.1: *3* itm.start
75 def start(self):
76 """Start the idle-time timer."""
77 self.timer = g.IdleTime(
78 self.on_idle,
79 delay=500,
80 tag='IdleTimeManager.on_idle')
81 if self.timer:
82 self.timer.start()
83 #@-others
84#@+node:ekr.20120209051836.10241: ** class LeoApp
85class LeoApp:
86 """A class representing the Leo application itself.
88 Ivars of this class are Leo's global variables."""
89 #@+others
90 #@+node:ekr.20150509193643.1: *3* app.Birth & startup
91 #@+node:ekr.20031218072017.1416: *4* app.__init__ (helpers contain language dicts)
92 def __init__(self):
93 """
94 Ctor for LeoApp class. These ivars are Leo's global vars.
96 leoGlobals.py contains global switches to be set by hand.
97 """
98 #@+<< LeoApp: command-line arguments >>
99 #@+node:ekr.20161028035755.1: *5* << LeoApp: command-line arguments >>
100 self.batchMode = False # True: run in batch mode.
101 self.debug = [] # A list of switches to be enabled.
102 self.diff = False # True: run Leo in diff mode.
103 self.enablePlugins = True # True: run start1 hook to load plugins. --no-plugins
104 self.failFast = False # True: Use the failfast option in unit tests.
105 self.gui = None # The gui class.
106 self.guiArgName = None # The gui name given in --gui option.
107 self.ipython_inited = False # True if leoIpython.py imports succeeded.
108 self.isTheme = False # True: load files as theme files (ignore myLeoSettings.leo).
109 self.listen_to_log_flag = False # True: execute listen-to-log command.
110 self.loaded_session = False # Set by startup logic to True if no files specified on the command line.
111 self.silentMode = False # True: no signon.
112 self.start_fullscreen = False # For qt_frame plugin.
113 self.start_maximized = False # For qt_frame plugin.
114 self.start_minimized = False # For qt_frame plugin.
115 self.trace_binding = None # The name of a binding to trace, or None.
116 self.trace_setting = None # The name of a setting to trace, or None.
117 self.translateToUpperCase = False # Never set to True.
118 self.useIpython = False # True: add support for IPython.
119 self.use_splash_screen = True # True: put up a splash screen.
120 #@-<< LeoApp: command-line arguments >>
121 #@+<< LeoApp: Debugging & statistics >>
122 #@+node:ekr.20161028035835.1: *5* << LeoApp: Debugging & statistics >>
123 self.debug_dict = {} # For general use.
124 self.disable_redraw = False # True: disable all redraws.
125 self.disableSave = False # May be set by plugins.
126 self.idle_timers = [] # A list of IdleTime instances, so they persist.
127 self.log_listener = None # The process created by the 'listen-for-log' command.
128 self.positions = 0 # The number of positions generated.
129 self.scanErrors = 0 # The number of errors seen by g.scanError.
130 self.structure_errors = 0 # Set by p.safeMoveToThreadNext.
131 self.statsDict = {} # dict used by g.stat, g.clear_stats, g.print_stats.
132 self.statsLockout = False # A lockout to prevent unbound recursion while gathering stats.
133 self.validate_outline = False # True: enables c.validate_outline. (slow)
134 #@-<< LeoApp: Debugging & statistics >>
135 #@+<< LeoApp: error messages >>
136 #@+node:ekr.20161028035902.1: *5* << LeoApp: error messages >>
137 self.menuWarningsGiven = False # True: supress warnings in menu code.
138 self.unicodeErrorGiven = True # True: suppres unicode tracebacks.
139 #@-<< LeoApp: error messages >>
140 #@+<< LeoApp: global directories >>
141 #@+node:ekr.20161028035924.1: *5* << LeoApp: global directories >>
142 self.extensionsDir = None # The leo/extensions directory
143 self.globalConfigDir = None # leo/config directory
144 self.globalOpenDir = None # The directory last used to open a file.
145 self.homeDir = None # The user's home directory.
146 self.homeLeoDir = None # The user's home/.leo directory.
147 self.leoEditorDir = None # The leo-editor directory.
148 self.loadDir = None # The leo/core directory.
149 self.machineDir = None # The machine-specific directory.
150 self.theme_directory = None # The directory from which the theme file was loaded, if any.
151 #@-<< LeoApp: global directories >>
152 #@+<< LeoApp: global data >>
153 #@+node:ekr.20161028035956.1: *5* << LeoApp: global data >>
154 self.atAutoNames = set() # The set of all @auto spellings.
155 self.atFileNames = set() # The set of all built-in @<file> spellings.
156 self.globalKillBuffer = [] # The global kill buffer.
157 self.globalRegisters = {} # The global register list.
158 self.leoID = None # The id part of gnx's.
159 self.loadedThemes = [] # List of loaded theme.leo files.
160 self.lossage = [] # List of last 100 keystrokes.
161 self.paste_c = None # The commander that pasted the last outline.
162 self.spellDict = None # The singleton PyEnchant spell dict.
163 self.numberOfUntitledWindows = 0 # Number of opened untitled windows.
164 self.windowList = [] # Global list of all frames.
165 self.realMenuNameDict = {} # Translations of menu names.
166 #@-<< LeoApp: global data >>
167 #@+<< LeoApp: global controller/manager objects >>
168 #@+node:ekr.20161028040028.1: *5* << LeoApp: global controller/manager objects >>
169 # 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 self.atAutoDict = {}
195 # Keys are @auto names, values are scanner classes.
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 g.app.recentFilesManager.writeRecentFilesFile(c)
1221 # Make sure .leoRecentFiles.txt is written.
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()
1233 # store cache, but don't close it.
1234 # This may remove frame from the window list.
1235 if frame in g.app.windowList:
1236 g.app.destroyWindow(frame)
1237 g.app.windowList.remove(frame)
1238 else:
1239 # #69.
1240 g.app.forgetOpenFile(fn=c.fileName())
1241 if g.app.windowList:
1242 c2 = new_c or g.app.windowList[0].c
1243 g.app.selectLeoWindow(c2)
1244 elif finish_quit and not g.unitTesting:
1245 g.app.finishQuit()
1246 return True # The window has been closed.
1247 #@+node:ekr.20031218072017.2612: *4* app.destroyAllOpenWithFiles
1248 def destroyAllOpenWithFiles(self):
1249 """Remove temp files created with the Open With command."""
1250 if 'shutdown' in g.app.debug:
1251 g.pr('destroyAllOpenWithFiles')
1252 if g.app.externalFilesController:
1253 g.app.externalFilesController.shut_down()
1254 g.app.externalFilesController = None
1255 #@+node:ekr.20031218072017.2615: *4* app.destroyWindow
1256 def destroyWindow(self, frame):
1257 """Destroy all ivars in a Leo frame."""
1258 if 'shutdown' in g.app.debug:
1259 g.pr(f"destroyWindow: {frame.c.shortFileName()}")
1260 if g.app.externalFilesController:
1261 g.app.externalFilesController.destroy_frame(frame)
1262 if frame in g.app.windowList:
1263 g.app.forgetOpenFile(frame.c.fileName())
1264 # force the window to go away now.
1265 # Important: this also destroys all the objects of the commander.
1266 frame.destroySelf()
1267 #@+node:ekr.20031218072017.1732: *4* app.finishQuit
1268 def finishQuit(self):
1269 # forceShutdown may already have fired the "end1" hook.
1270 assert self == g.app, repr(g.app)
1271 trace = 'shutdown' in g.app.debug
1272 if trace:
1273 g.pr('finishQuit: killed:', g.app.killed)
1274 if not g.app.killed:
1275 g.doHook("end1")
1276 if g.app.global_cacher: # #1766.
1277 g.app.global_cacher.commit_and_close()
1278 if g.app.commander_cacher: # #1766.
1279 g.app.commander_cacher.commit()
1280 g.app.commander_cacher.close()
1281 if g.app.ipk:
1282 g.app.ipk.cleanup_consoles()
1283 g.app.destroyAllOpenWithFiles()
1284 if hasattr(g.app, 'pyzo_close_handler'):
1285 # pylint: disable=no-member
1286 g.app.pyzo_close_handler()
1287 # Disable all further hooks and events.
1288 # Alas, "idle" events can still be called
1289 # even after the following code.
1290 g.app.killed = True
1291 if g.app.gui:
1292 g.app.gui.destroySelf() # Calls qtApp.quit()
1293 #@+node:ekr.20031218072017.2616: *4* app.forceShutdown
1294 def forceShutdown(self):
1295 """
1296 Forces an immediate shutdown of Leo at any time.
1298 In particular, may be called from plugins during startup.
1299 """
1300 trace = 'shutdown' in g.app.debug
1301 app = self
1302 if trace:
1303 g.pr('forceShutdown')
1304 for c in app.commanders():
1305 app.forgetOpenFile(c.fileName())
1306 # Wait until everything is quiet before really quitting.
1307 if trace:
1308 g.pr('forceShutdown: before end1')
1309 g.doHook("end1")
1310 if trace:
1311 g.pr('forceShutdown: after end1')
1312 self.log = None # Disable writeWaitingLog
1313 self.killed = True # Disable all further hooks.
1314 for w in self.windowList[:]:
1315 if trace:
1316 g.pr(f"forceShutdown: {w}")
1317 self.destroyWindow(w)
1318 if trace:
1319 g.pr('before finishQuit')
1320 self.finishQuit()
1321 #@+node:ekr.20031218072017.2617: *4* app.onQuit
1322 @cmd('exit-leo')
1323 @cmd('quit-leo')
1324 def onQuit(self, event=None):
1325 """Exit Leo, prompting to save unsaved outlines first."""
1326 if 'shutdown' in g.app.debug:
1327 g.trace()
1328 # #2433 - use the same method as clicking on the close box.
1329 g.app.gui.close_event(QCloseEvent()) # type:ignore
1330 #@+node:ville.20090602181814.6219: *3* app.commanders
1331 def commanders(self):
1332 """ Return list of currently active controllers """
1333 return [f.c for f in g.app.windowList]
1334 #@+node:ekr.20120427064024.10068: *3* app.Detecting already-open files
1335 #@+node:ekr.20120427064024.10064: *4* app.checkForOpenFile
1336 def checkForOpenFile(self, c, fn):
1337 """Warn if fn is already open and add fn to already_open_files list."""
1338 d, tag = g.app.db, 'open-leo-files'
1339 if g.app.reverting:
1340 # #302: revert to saved doesn't reset external file change monitoring
1341 g.app.already_open_files = []
1342 if (d is None or
1343 g.unitTesting or
1344 g.app.batchMode or
1345 g.app.reverting or
1346 g.app.inBridge
1347 ):
1348 return
1349 # #1519: check os.path.exists.
1350 aList = g.app.db.get(tag) or [] # A list of normalized file names.
1351 if any(os.path.exists(z) and os.path.samefile(z, fn) for z in aList):
1352 # The file may be open in another copy of Leo, or not:
1353 # another Leo may have been killed prematurely.
1354 # Put the file on the global list.
1355 # A dialog will warn the user such files later.
1356 fn = os.path.normpath(fn)
1357 if fn not in g.app.already_open_files:
1358 g.es('may be open in another Leo:', color='red')
1359 g.es(fn)
1360 g.app.already_open_files.append(fn)
1361 else:
1362 g.app.rememberOpenFile(fn)
1363 #@+node:ekr.20120427064024.10066: *4* app.forgetOpenFile
1364 def forgetOpenFile(self, fn):
1365 """
1366 Remove fn from g.app.db, so that is no longer considered open.
1367 """
1368 trace = 'shutdown' in g.app.debug
1369 d, tag = g.app.db, 'open-leo-files'
1370 if not d or not fn:
1371 return # #69.
1372 aList = d.get(tag) or []
1373 fn = os.path.normpath(fn)
1374 if fn in aList:
1375 aList.remove(fn)
1376 if trace:
1377 g.pr(f"forgetOpenFile: {g.shortFileName(fn)}")
1378 d[tag] = aList
1380 #@+node:ekr.20120427064024.10065: *4* app.rememberOpenFile
1381 def rememberOpenFile(self, fn):
1383 #
1384 # Do not call g.trace, etc. here.
1385 d, tag = g.app.db, 'open-leo-files'
1386 if d is None or g.unitTesting or g.app.batchMode or g.app.reverting:
1387 pass
1388 elif g.app.preReadFlag:
1389 pass
1390 else:
1391 aList = d.get(tag) or []
1392 # It's proper to add duplicates to this list.
1393 aList.append(os.path.normpath(fn))
1394 d[tag] = aList
1395 #@+node:ekr.20150621062355.1: *4* app.runAlreadyOpenDialog
1396 def runAlreadyOpenDialog(self, c):
1397 """Warn about possibly already-open files."""
1398 if g.app.already_open_files:
1399 aList = sorted(set(g.app.already_open_files))
1400 g.app.already_open_files = []
1401 g.app.gui.dismiss_splash_screen()
1402 message = (
1403 'The following files may already be open\n'
1404 'in another copy of Leo:\n\n' +
1405 '\n'.join(aList))
1406 g.app.gui.runAskOkDialog(c,
1407 title='Already Open Files',
1408 message=message,
1409 text="Ok")
1410 #@+node:ekr.20171127111141.1: *3* app.Import utils
1411 #@+node:ekr.20140727180847.17985: *4* app.scanner_for_at_auto
1412 def scanner_for_at_auto(self, c, p, **kwargs):
1413 """A factory returning a scanner function for p, an @auto node."""
1414 d = g.app.atAutoDict
1415 for key in d:
1416 # pylint: disable=cell-var-from-loop
1417 func = d.get(key)
1418 if func and g.match_word(p.h, 0, key):
1419 return func
1420 return None
1421 #@+node:ekr.20140130172810.15471: *4* app.scanner_for_ext
1422 def scanner_for_ext(self, c, ext, **kwargs):
1423 """A factory returning a scanner function for the given file extension."""
1424 return g.app.classDispatchDict.get(ext)
1425 #@+node:ekr.20170429152049.1: *3* app.listenToLog
1426 @cmd('listen-to-log')
1427 @cmd('log-listen')
1428 def listenToLog(self, event=None):
1429 """
1430 A socket listener, listening on localhost. See:
1431 https://docs.python.org/2/howto/logging-cookbook.html#sending-and-receiving-logging-events-across-a-network
1433 Start this listener first, then start the broadcaster.
1435 leo/plugins/cursesGui2.py is a typical broadcaster.
1436 """
1437 app = self
1438 # Kill any previous listener.
1439 if app.log_listener:
1440 g.es_print('Killing previous listener')
1441 try:
1442 app.log_listener.kill()
1443 except Exception:
1444 g.es_exception()
1445 app.log_listener = None
1446 # Start a new listener.
1447 g.es_print('Starting log_listener.py')
1448 path = g.os_path_finalize_join(app.loadDir,
1449 '..', 'external', 'log_listener.py')
1450 app.log_listener = subprocess.Popen(
1451 [sys.executable, path],
1452 shell=False,
1453 universal_newlines=True,
1454 )
1455 #@+node:ekr.20171118024827.1: *3* app.makeAllBindings
1456 def makeAllBindings(self):
1457 """
1458 LeoApp.makeAllBindings:
1460 Call c.k.makeAllBindings for all open commanders c.
1461 """
1462 app = self
1463 for c in app.commanders():
1464 c.k.makeAllBindings()
1465 #@+node:ekr.20031218072017.2188: *3* app.newCommander
1466 def newCommander(self, fileName,
1467 gui=None,
1468 parentFrame=None,
1469 previousSettings=None,
1470 relativeFileName=None,
1471 ):
1472 """Create a commander and its view frame for the Leo main window."""
1473 # Create the commander and its subcommanders.
1474 # This takes about 3/4 sec when called by the leoBridge module.
1475 # Timeit reports 0.0175 sec when using a nullGui.
1476 from leo.core import leoCommands
1477 c = leoCommands.Commands(fileName,
1478 gui=gui,
1479 parentFrame=parentFrame,
1480 previousSettings=previousSettings,
1481 relativeFileName=relativeFileName,
1482 )
1483 return c
1484 #@+node:ekr.20120304065838.15588: *3* app.selectLeoWindow
1485 def selectLeoWindow(self, c):
1486 frame = c.frame
1487 frame.deiconify()
1488 frame.lift()
1489 c.setLog()
1490 master = getattr(frame.top, 'leo_master', None)
1491 if master:
1492 # master is a TabbedTopLevel.
1493 # Selecting the new tab ensures focus is set.
1494 master.select(c)
1495 if 1:
1496 c.initialFocusHelper()
1497 else:
1498 c.bodyWantsFocus()
1499 c.outerUpdate()
1500 #@-others
1501#@+node:ekr.20120209051836.10242: ** class LoadManager
1502class LoadManager:
1503 """A class to manage loading .leo files, including configuration files."""
1504 #@+others
1505 #@+node:ekr.20120214060149.15851: *3* LM.ctor
1506 def __init__(self):
1508 # Global settings & shortcuts dicts...
1509 # The are the defaults for computing settings and shortcuts for all loaded files.
1511 # A g.TypedDict: the join of settings in leoSettings.leo & myLeoSettings.leo
1512 self.globalSettingsDict = None
1513 # A g.TypedDict: the join of shortcuts in leoSettings.leo & myLeoSettings.leo.
1514 self.globalBindingsDict = None
1516 # LoadManager ivars corresponding to user options...
1518 self.files = [] # List of files to be loaded.
1519 self.options = {} # Keys are option names; values are user options.
1520 self.old_argv = [] # A copy of sys.argv for debugging.
1522 # True when more files remain on the command line to be loaded.
1523 # If the user is answering "No" to each file as Leo asks
1524 # "file already open, open again".
1525 # This must be False for a complete exit to be appropriate
1526 # (finish_quit=True param for closeLeoWindow())
1527 self.more_cmdline_files = False
1529 # Themes...
1530 self.leo_settings_c = None
1531 self.leo_settings_path = None
1532 self.my_settings_c = None
1533 self.my_settings_path = None
1534 self.theme_c = None # #1374.
1535 self.theme_path = None
1536 #@+node:ekr.20120211121736.10812: *3* LM.Directory & file utils
1537 #@+node:ekr.20120219154958.10481: *4* LM.completeFileName
1538 def completeFileName(self, fileName):
1539 fileName = g.toUnicode(fileName)
1540 fileName = g.os_path_finalize(fileName)
1541 # 2011/10/12: don't add .leo to *any* file.
1542 return fileName
1543 #@+node:ekr.20120209051836.10372: *4* LM.computeLeoSettingsPath
1544 def computeLeoSettingsPath(self):
1545 """Return the full path to leoSettings.leo."""
1546 # lm = self
1547 join = g.os_path_finalize_join
1548 settings_fn = 'leoSettings.leo'
1549 table = (
1550 # First, leoSettings.leo in the home directories.
1551 join(g.app.homeDir, settings_fn),
1552 join(g.app.homeLeoDir, settings_fn),
1553 # Last, leoSettings.leo in leo/config directory.
1554 join(g.app.globalConfigDir, settings_fn)
1555 )
1556 for path in table:
1557 if g.os_path_exists(path):
1558 break
1559 else:
1560 path = None
1561 return path
1562 #@+node:ekr.20120209051836.10373: *4* LM.computeMyLeoSettingsPath
1563 def computeMyLeoSettingsPath(self):
1564 """
1565 Return the full path to myLeoSettings.leo.
1567 The "footnote": Get the local directory from lm.files[0]
1568 """
1569 lm = self
1570 join = g.os_path_finalize_join
1571 settings_fn = 'myLeoSettings.leo'
1572 # This seems pointless: we need a machine *directory*.
1573 # For now, however, we'll keep the existing code as is.
1574 machine_fn = lm.computeMachineName() + settings_fn
1575 # First, compute the directory of the first loaded file.
1576 # All entries in lm.files are full, absolute paths.
1577 localDir = g.os_path_dirname(lm.files[0]) if lm.files else ''
1578 table = (
1579 # First, myLeoSettings.leo in the local directory
1580 join(localDir, settings_fn),
1581 # Next, myLeoSettings.leo in the home directories.
1582 join(g.app.homeDir, settings_fn),
1583 join(g.app.homeLeoDir, settings_fn),
1584 # Next, <machine-name>myLeoSettings.leo in the home directories.
1585 join(g.app.homeDir, machine_fn),
1586 join(g.app.homeLeoDir, machine_fn),
1587 # Last, leoSettings.leo in leo/config directory.
1588 join(g.app.globalConfigDir, settings_fn),
1589 )
1590 for path in table:
1591 if g.os_path_exists(path):
1592 break
1593 else:
1594 path = None
1595 return path
1596 #@+node:ekr.20120209051836.10252: *4* LM.computeStandardDirectories & helpers
1597 def computeStandardDirectories(self):
1598 """
1599 Compute the locations of standard directories and
1600 set the corresponding ivars.
1601 """
1602 lm = self
1603 join = os.path.join
1604 g.app.loadDir = lm.computeLoadDir()
1605 g.app.globalConfigDir = lm.computeGlobalConfigDir()
1606 g.app.homeDir = lm.computeHomeDir()
1607 g.app.homeLeoDir = lm.computeHomeLeoDir()
1608 g.app.leoDir = lm.computeLeoDir()
1609 # These use g.app.loadDir...
1610 g.app.extensionsDir = join(g.app.loadDir, '..', 'extensions')
1611 g.app.leoEditorDir = join(g.app.loadDir, '..', '..')
1612 g.app.testDir = join(g.app.loadDir, '..', 'test')
1613 #@+node:ekr.20120209051836.10253: *5* LM.computeGlobalConfigDir
1614 def computeGlobalConfigDir(self):
1615 leo_config_dir = getattr(sys, 'leo_config_directory', None)
1616 if leo_config_dir:
1617 theDir = leo_config_dir
1618 else:
1619 theDir = os.path.join(g.app.loadDir, "..", "config")
1620 if theDir:
1621 theDir = os.path.abspath(theDir)
1622 if not theDir or not g.os_path_exists(theDir) or not g.os_path_isdir(theDir):
1623 theDir = None
1624 return theDir
1625 #@+node:ekr.20120209051836.10254: *5* LM.computeHomeDir
1626 def computeHomeDir(self):
1627 """Returns the user's home directory."""
1628 # Windows searches the HOME, HOMEPATH and HOMEDRIVE
1629 # environment vars, then gives up.
1630 home = os.path.expanduser("~")
1631 if home and len(home) > 1 and home[0] == '%' and home[-1] == '%':
1632 # Get the indirect reference to the true home.
1633 home = os.getenv(home[1:-1], default=None)
1634 if home:
1635 # Important: This returns the _working_ directory if home is None!
1636 # This was the source of the 4.3 .leoID.txt problems.
1637 home = g.os_path_finalize(home)
1638 if (not g.os_path_exists(home) or not g.os_path_isdir(home)):
1639 home = None
1640 return home
1641 #@+node:ekr.20120209051836.10260: *5* LM.computeHomeLeoDir
1642 def computeHomeLeoDir(self):
1643 # lm = self
1644 homeLeoDir = g.os_path_finalize_join(g.app.homeDir, '.leo')
1645 if g.os_path_exists(homeLeoDir):
1646 return homeLeoDir
1647 ok = g.makeAllNonExistentDirectories(homeLeoDir)
1648 return homeLeoDir if ok else '' # #1450
1649 #@+node:ekr.20120209051836.10255: *5* LM.computeLeoDir
1650 def computeLeoDir(self):
1651 # lm = self
1652 loadDir = g.app.loadDir
1653 return g.os_path_dirname(loadDir)
1654 # We don't want the result in sys.path
1655 #@+node:ekr.20120209051836.10256: *5* LM.computeLoadDir
1656 def computeLoadDir(self):
1657 """Returns the directory containing leo.py."""
1658 try:
1659 # Fix a hangnail: on Windows the drive letter returned by
1660 # __file__ is randomly upper or lower case!
1661 # The made for an ugly recent files list.
1662 path = g.__file__ # was leo.__file__
1663 if path:
1664 # Possible fix for bug 735938:
1665 # Do the following only if path exists.
1666 #@+<< resolve symlinks >>
1667 #@+node:ekr.20120209051836.10257: *6* << resolve symlinks >>
1668 if path.endswith('pyc'):
1669 srcfile = path[:-1]
1670 if os.path.islink(srcfile):
1671 path = os.path.realpath(srcfile)
1672 #@-<< resolve symlinks >>
1673 if sys.platform == 'win32':
1674 if len(path) > 2 and path[1] == ':':
1675 # Convert the drive name to upper case.
1676 path = path[0].upper() + path[1:]
1677 path = g.os_path_finalize(path)
1678 loadDir = g.os_path_dirname(path)
1679 else: loadDir = None
1680 if (
1681 not loadDir or
1682 not g.os_path_exists(loadDir) or
1683 not g.os_path_isdir(loadDir)
1684 ):
1685 loadDir = os.getcwd()
1686 # From Marc-Antoine Parent.
1687 if loadDir.endswith("Contents/Resources"):
1688 loadDir += "/leo/plugins"
1689 else:
1690 g.pr("Exception getting load directory")
1691 loadDir = g.os_path_finalize(loadDir)
1692 return loadDir
1693 except Exception:
1694 print("Exception getting load directory")
1695 raise
1696 #@+node:ekr.20120213164030.10697: *5* LM.computeMachineName
1697 def computeMachineName(self):
1698 """Return the name of the current machine, i.e, HOSTNAME."""
1699 # This is prepended to leoSettings.leo or myLeoSettings.leo
1700 # to give the machine-specific setting name.
1701 # How can this be worth doing??
1702 try:
1703 name = os.getenv('HOSTNAME')
1704 if not name:
1705 name = os.getenv('COMPUTERNAME')
1706 if not name:
1707 import socket
1708 name = socket.gethostname()
1709 except Exception:
1710 name = ''
1711 return name
1712 #@+node:ekr.20180318120148.1: *4* LM.computeThemeDirectories
1713 def computeThemeDirectories(self):
1714 """
1715 Return a list of *existing* directories that might contain theme .leo files.
1716 """
1717 join = g.os_path_finalize_join
1718 home = g.app.homeDir
1719 leo = join(g.app.loadDir, '..')
1720 table = [
1721 home,
1722 join(home, 'themes'),
1723 join(home, '.leo'),
1724 join(home, '.leo', 'themes'),
1725 join(leo, 'themes'),
1726 ]
1727 return [g.os_path_normslashes(z) for z in table if g.os_path_exists(z)]
1728 # Make sure home has normalized slashes.
1729 #@+node:ekr.20180318133620.1: *4* LM.computeThemeFilePath & helper
1730 def computeThemeFilePath(self):
1731 """
1732 Return the absolute path to the theme .leo file, resolved using the search order for themes.
1734 1. Use the --theme command-line option if it exists.
1736 2. Otherwise, preload the first .leo file.
1737 Load the file given by @string theme-name setting.
1739 3. Finally, look up the @string theme-name in the already-loaded, myLeoSettings.leo.
1740 Load the file if setting exists. Otherwise return None.
1741 """
1742 trace = 'themes' in g.app.db
1743 lm = self
1744 resolve = self.resolve_theme_path
1745 #
1746 # Step 1: Use the --theme command-line options if it exists
1747 path = resolve(lm.options.get('theme_path'), tag='--theme')
1748 if path:
1749 # Caller (LM.readGlobalSettingsFiles) sets lm.theme_path
1750 if trace:
1751 g.trace('--theme:', path)
1752 return path
1753 #
1754 # Step 2: look for the @string theme-name setting in the first loaded file.
1755 path = lm.files and lm.files[0]
1756 if path and g.os_path_exists(path):
1757 # Tricky: we must call lm.computeLocalSettings *here*.
1758 theme_c = lm.openSettingsFile(path)
1759 if theme_c:
1760 settings_d, junk_shortcuts_d = lm.computeLocalSettings(
1761 c=theme_c,
1762 settings_d=lm.globalSettingsDict,
1763 bindings_d=lm.globalBindingsDict,
1764 localFlag=False,
1765 )
1766 setting = settings_d.get_string_setting('theme-name')
1767 if setting:
1768 tag = theme_c.shortFileName()
1769 path = resolve(setting, tag=tag)
1770 if path:
1771 # Caller (LM.readGlobalSettingsFiles) sets lm.theme_path
1772 if trace:
1773 g.trace("First loaded file", theme_c.shortFileName(), path)
1774 return path
1775 #
1776 # Step 3: use the @string theme-name setting in myLeoSettings.leo.
1777 # Note: the setting should *never* appear in leoSettings.leo!
1778 setting = lm.globalSettingsDict.get_string_setting('theme-name')
1779 tag = 'myLeoSettings.leo'
1780 path = resolve(setting, tag=tag)
1781 if trace:
1782 g.trace("myLeoSettings.leo", path)
1783 return path
1784 #@+node:ekr.20180321124503.1: *5* LM.resolve_theme_path
1785 def resolve_theme_path(self, fn, tag):
1786 """Search theme directories for the given .leo file."""
1787 if not fn or fn.lower().strip() == 'none':
1788 return None
1789 if not fn.endswith('.leo'):
1790 fn += '.leo'
1791 for directory in self.computeThemeDirectories():
1792 path = g.os_path_join(directory, fn) # Normalizes slashes, etc.
1793 if g.os_path_exists(path):
1794 return path
1795 print(f"theme .leo file not found: {fn}")
1796 return None
1797 #@+node:ekr.20120211121736.10772: *4* LM.computeWorkbookFileName
1798 def computeWorkbookFileName(self):
1799 """
1800 Return full path to the workbook.
1802 Return None if testing, or in batch mode, or if the containing
1803 directory does not exist.
1804 """
1805 # lm = self
1806 # Never create a workbook during unit tests or in batch mode.
1807 if g.unitTesting or g.app.batchMode:
1808 return None
1809 fn = g.app.config.getString(setting='default_leo_file') or '~/.leo/workbook.leo'
1810 fn = g.os_path_finalize(fn)
1811 directory = g.os_path_finalize(os.path.dirname(fn))
1812 # #1415.
1813 return fn if os.path.exists(directory) else None
1814 #@+node:ekr.20120219154958.10485: *4* LM.reportDirectories
1815 def reportDirectories(self):
1816 """Report directories."""
1817 # The cwd changes later, so it would be misleading to report it here.
1818 for kind, theDir in (
1819 ('home', g.app.homeDir),
1820 ('leo-editor', g.app.leoEditorDir),
1821 ('load', g.app.loadDir),
1822 ('config', g.app.globalConfigDir),
1823 ):
1824 # g.blue calls g.es_print, and that's annoying.
1825 g.es(f"{kind:>10}:", os.path.normpath(theDir), color='blue')
1826 #@+node:ekr.20120215062153.10740: *3* LM.Settings
1827 #@+node:ekr.20120130101219.10182: *4* LM.computeBindingLetter
1828 def computeBindingLetter(self, c, path):
1829 lm = self
1830 if not path:
1831 return 'D'
1832 path = path.lower()
1833 table = (
1834 ('M', 'myLeoSettings.leo'),
1835 (' ', 'leoSettings.leo'),
1836 ('F', c.shortFileName()),
1837 )
1838 for letter, path2 in table:
1839 if path2 and path.endswith(path2.lower()):
1840 return letter
1841 if lm.theme_path and path.endswith(lm.theme_path.lower()):
1842 return 'T'
1843 if path == 'register-command' or path.find('mode') > -1:
1844 return '@'
1845 return 'D'
1846 #@+node:ekr.20120223062418.10421: *4* LM.computeLocalSettings
1847 def computeLocalSettings(self, c, settings_d, bindings_d, localFlag):
1848 """
1849 Merge the settings dicts from c's outline into *new copies of*
1850 settings_d and bindings_d.
1851 """
1852 lm = self
1853 shortcuts_d2, settings_d2 = lm.createSettingsDicts(c, localFlag)
1854 if not bindings_d: # #1766: unit tests.
1855 settings_d, bindings_d = lm.createDefaultSettingsDicts()
1856 if settings_d2:
1857 if g.app.trace_setting:
1858 key = g.app.config.munge(g.app.trace_setting)
1859 val = settings_d2.d.get(key)
1860 if val:
1861 fn = g.shortFileName(val.path)
1862 g.es_print(
1863 f"--trace-setting: in {fn:20}: "
1864 f"@{val.kind} {g.app.trace_setting}={val.val}")
1865 settings_d = settings_d.copy()
1866 settings_d.update(settings_d2)
1867 if shortcuts_d2:
1868 bindings_d = lm.mergeShortcutsDicts(c, bindings_d, shortcuts_d2, localFlag)
1869 return settings_d, bindings_d
1870 #@+node:ekr.20121126202114.3: *4* LM.createDefaultSettingsDicts
1871 def createDefaultSettingsDicts(self):
1872 """Create lm.globalSettingsDict & lm.globalBindingsDict."""
1873 settings_d = g.app.config.defaultsDict
1874 assert isinstance(settings_d, g.TypedDict), settings_d
1875 settings_d.setName('lm.globalSettingsDict')
1876 bindings_d = g.TypedDict( # was TypedDictOfLists.
1877 name='lm.globalBindingsDict',
1878 keyType=type('s'),
1879 valType=g.BindingInfo,
1880 )
1881 return settings_d, bindings_d
1882 #@+node:ekr.20120214165710.10726: *4* LM.createSettingsDicts
1883 def createSettingsDicts(self, c, localFlag):
1885 from leo.core import leoConfig
1886 if c:
1887 # returns the *raw* shortcutsDict, not a *merged* shortcuts dict.
1888 parser = leoConfig.SettingsTreeParser(c, localFlag)
1889 shortcutsDict, settingsDict = parser.traverse()
1890 return shortcutsDict, settingsDict
1891 return None, None
1892 #@+node:ekr.20120223062418.10414: *4* LM.getPreviousSettings
1893 def getPreviousSettings(self, fn):
1894 """
1895 Return the settings in effect for fn. Typically, this involves
1896 pre-reading fn.
1897 """
1898 lm = self
1899 settingsName = f"settings dict for {g.shortFileName(fn)}"
1900 shortcutsName = f"shortcuts dict for {g.shortFileName(fn)}"
1901 # A special case: settings in leoSettings.leo do *not* override
1902 # the global settings, that is, settings in myLeoSettings.leo.
1903 isLeoSettings = fn and g.shortFileName(fn).lower() == 'leosettings.leo'
1904 exists = g.os_path_exists(fn)
1905 if fn and exists and lm.isLeoFile(fn) and not isLeoSettings:
1906 # Open the file usinging a null gui.
1907 try:
1908 g.app.preReadFlag = True
1909 c = lm.openSettingsFile(fn)
1910 finally:
1911 g.app.preReadFlag = False
1912 # Merge the settings from c into *copies* of the global dicts.
1913 d1, d2 = lm.computeLocalSettings(c,
1914 lm.globalSettingsDict,
1915 lm.globalBindingsDict,
1916 localFlag=True)
1917 # d1 and d2 are copies.
1918 d1.setName(settingsName)
1919 d2.setName(shortcutsName)
1920 return PreviousSettings(d1, d2)
1921 #
1922 # The file does not exist, or is not valid.
1923 # Get the settings from the globals settings dicts.
1924 if lm.globalSettingsDict and lm.globalBindingsDict: # #1766.
1925 d1 = lm.globalSettingsDict.copy(settingsName)
1926 d2 = lm.globalBindingsDict.copy(shortcutsName)
1927 else:
1928 d1 = d2 = None
1929 return PreviousSettings(d1, d2)
1930 #@+node:ekr.20120214132927.10723: *4* LM.mergeShortcutsDicts & helpers
1931 def mergeShortcutsDicts(self, c, old_d, new_d, localFlag):
1932 """
1933 Create a new dict by overriding all shortcuts in old_d by shortcuts in new_d.
1935 Both old_d and new_d remain unchanged.
1936 """
1937 lm = self
1938 if not old_d:
1939 return new_d
1940 if not new_d:
1941 return old_d
1942 bi_list = new_d.get(g.app.trace_setting)
1943 if bi_list:
1944 # This code executed only if g.app.trace_setting exists.
1945 for bi in bi_list:
1946 fn = bi.kind.split(' ')[-1]
1947 stroke = c.k.prettyPrintKey(bi.stroke)
1948 if bi.pane and bi.pane != 'all':
1949 pane = f" in {bi.pane} panes"
1950 else:
1951 pane = ''
1952 inverted_old_d = lm.invert(old_d)
1953 inverted_new_d = lm.invert(new_d)
1954 # #510 & #327: always honor --trace-binding here.
1955 if g.app.trace_binding:
1956 binding = g.app.trace_binding
1957 # First, see if the binding is for a command. (Doesn't work for plugin commands).
1958 if localFlag and binding in c.k.killedBindings:
1959 g.es_print(
1960 f"--trace-binding: {c.shortFileName()} "
1961 f"sets {binding} to None")
1962 elif localFlag and binding in c.commandsDict:
1963 d = c.k.computeInverseBindingDict()
1964 g.trace(
1965 f"--trace-binding: {c.shortFileName():20} "
1966 f"binds {binding} to {d.get(binding) or []}")
1967 else:
1968 binding = g.app.trace_binding
1969 stroke = g.KeyStroke(binding)
1970 bi_list = inverted_new_d.get(stroke)
1971 if bi_list:
1972 print('')
1973 for bi in bi_list:
1974 fn = bi.kind.split(' ')[-1] # bi.kind #
1975 stroke2 = c.k.prettyPrintKey(stroke)
1976 if bi.pane and bi.pane != 'all':
1977 pane = f" in {bi.pane} panes"
1978 else:
1979 pane = ''
1980 g.es_print(
1981 f"--trace-binding: {fn:20} binds {stroke2} "
1982 f"to {bi.commandName:>20}{pane}")
1983 print('')
1984 # Fix bug 951921: check for duplicate shortcuts only in the new file.
1985 lm.checkForDuplicateShortcuts(c, inverted_new_d)
1986 inverted_old_d.update(inverted_new_d) # Updates inverted_old_d in place.
1987 result = lm.uninvert(inverted_old_d)
1988 return result
1989 #@+node:ekr.20120311070142.9904: *5* LM.checkForDuplicateShortcuts
1990 def checkForDuplicateShortcuts(self, c, d):
1991 """
1992 Check for duplicates in an "inverted" dictionary d
1993 whose keys are strokes and whose values are lists of BindingInfo nodes.
1995 Duplicates happen only if panes conflict.
1996 """
1997 # Fix bug 951921: check for duplicate shortcuts only in the new file.
1998 for ks in sorted(list(d.keys())):
1999 duplicates, panes = [], ['all']
2000 aList = d.get(ks) # A list of bi objects.
2001 aList2 = [z for z in aList if not z.pane.startswith('mode')]
2002 if len(aList) > 1:
2003 for bi in aList2:
2004 if bi.pane in panes:
2005 duplicates.append(bi)
2006 else:
2007 panes.append(bi.pane)
2008 if duplicates:
2009 bindings = list(set([z.stroke.s for z in duplicates]))
2010 if len(bindings) == 1:
2011 kind = 'duplicate, (not conflicting)'
2012 else:
2013 kind = 'conflicting'
2014 g.es_print(f"{kind} key bindings in {c.shortFileName()}")
2015 for bi in aList2:
2016 g.es_print(f"{bi.pane:6} {bi.stroke.s} {bi.commandName}")
2017 #@+node:ekr.20120214132927.10724: *5* LM.invert
2018 def invert(self, d):
2019 """
2020 Invert a shortcut dict whose keys are command names,
2021 returning a dict whose keys are strokes.
2022 """
2023 result = g.TypedDict( # was TypedDictOfLists.
2024 name=f"inverted {d.name()}",
2025 keyType=g.KeyStroke,
2026 valType=g.BindingInfo,
2027 )
2028 for commandName in d.keys():
2029 for bi in d.get(commandName, []):
2030 stroke = bi.stroke # This is canonicalized.
2031 bi.commandName = commandName # Add info.
2032 assert stroke
2033 result.add_to_list(stroke, bi)
2034 return result
2035 #@+node:ekr.20120214132927.10725: *5* LM.uninvert
2036 def uninvert(self, d):
2037 """
2038 Uninvert an inverted shortcut dict whose keys are strokes,
2039 returning a dict whose keys are command names.
2040 """
2041 assert d.keyType == g.KeyStroke, d.keyType
2042 result = g.TypedDict( # was TypedDictOfLists.
2043 name=f"uninverted {d.name()}",
2044 keyType=type('commandName'),
2045 valType=g.BindingInfo,
2046 )
2047 for stroke in d.keys():
2048 for bi in d.get(stroke, []):
2049 commandName = bi.commandName
2050 assert commandName
2051 result.add_to_list(commandName, bi)
2052 return result
2053 #@+node:ekr.20120222103014.10312: *4* LM.openSettingsFile
2054 def openSettingsFile(self, fn):
2055 """
2056 Open a settings file with a null gui. Return the commander.
2058 The caller must init the c.config object.
2059 """
2060 lm = self
2061 if not fn:
2062 return None
2063 theFile = lm.openAnyLeoFile(fn)
2064 if not theFile:
2065 return None # Fix #843.
2066 if not any([g.unitTesting, g.app.silentMode, g.app.batchMode]):
2067 # This occurs early in startup, so use the following.
2068 s = f"reading settings in {os.path.normpath(fn)}"
2069 if 'startup' in g.app.debug:
2070 print(s)
2071 g.es(s, color='blue')
2072 # A useful trace.
2073 # g.trace('%20s' % g.shortFileName(fn), g.callers(3))
2074 # Changing g.app.gui here is a major hack. It is necessary.
2075 oldGui = g.app.gui
2076 g.app.gui = g.app.nullGui
2077 c = g.app.newCommander(fn)
2078 frame = c.frame
2079 frame.log.enable(False)
2080 g.app.lockLog()
2081 g.app.openingSettingsFile = True
2082 try:
2083 ok = c.fileCommands.openLeoFile(theFile, fn,
2084 readAtFileNodesFlag=False, silent=True)
2085 # closes theFile.
2086 finally:
2087 g.app.openingSettingsFile = False
2088 g.app.unlockLog()
2089 c.openDirectory = frame.openDirectory = g.os_path_dirname(fn)
2090 g.app.gui = oldGui
2091 return c if ok else None
2092 #@+node:ekr.20120213081706.10382: *4* LM.readGlobalSettingsFiles
2093 def readGlobalSettingsFiles(self):
2094 """
2095 Read leoSettings.leo and myLeoSettings.leo using a null gui.
2097 New in Leo 6.1: this sets ivars for the ActiveSettingsOutline class.
2098 """
2099 trace = 'themes' in g.app.debug
2100 lm = self
2101 # Open the standard settings files with a nullGui.
2102 # Important: their commanders do not exist outside this method!
2103 old_commanders = g.app.commanders()
2104 lm.leo_settings_path = lm.computeLeoSettingsPath()
2105 lm.my_settings_path = lm.computeMyLeoSettingsPath()
2106 lm.leo_settings_c = lm.openSettingsFile(self.leo_settings_path)
2107 lm.my_settings_c = lm.openSettingsFile(self.my_settings_path)
2108 commanders = [lm.leo_settings_c, lm.my_settings_c]
2109 commanders = [z for z in commanders if z]
2110 settings_d, bindings_d = lm.createDefaultSettingsDicts()
2111 for c in commanders:
2112 # Merge the settings dicts from c's outline into
2113 # *new copies of* settings_d and bindings_d.
2114 settings_d, bindings_d = lm.computeLocalSettings(
2115 c, settings_d, bindings_d, localFlag=False)
2116 # Adjust the name.
2117 bindings_d.setName('lm.globalBindingsDict')
2118 lm.globalSettingsDict = settings_d
2119 lm.globalBindingsDict = bindings_d
2120 # Add settings from --theme or @string theme-name files.
2121 # This must be done *after* reading myLeoSettigns.leo.
2122 lm.theme_path = lm.computeThemeFilePath()
2123 if lm.theme_path:
2124 lm.theme_c = lm.openSettingsFile(lm.theme_path)
2125 if lm.theme_c:
2126 # Merge theme_c's settings into globalSettingsDict.
2127 settings_d, junk_shortcuts_d = lm.computeLocalSettings(
2128 lm.theme_c, settings_d, bindings_d, localFlag=False)
2129 lm.globalSettingsDict = settings_d
2130 # Set global var used by the StyleSheetManager.
2131 g.app.theme_directory = g.os_path_dirname(lm.theme_path)
2132 if trace:
2133 g.trace('g.app.theme_directory', g.app.theme_directory)
2134 # Clear the cache entries for the commanders.
2135 # This allows this method to be called outside the startup logic.
2136 for c in commanders:
2137 if c not in old_commanders:
2138 g.app.forgetOpenFile(c.fileName())
2139 #@+node:ekr.20120214165710.10838: *4* LM.traceSettingsDict
2140 def traceSettingsDict(self, d, verbose=False):
2141 if verbose:
2142 print(d)
2143 for key in sorted(list(d.keys())):
2144 gs = d.get(key)
2145 print(f"{key:35} {g.shortFileName(gs.path):17} {gs.val}")
2146 if d:
2147 print('')
2148 else:
2149 # print(d)
2150 print(f"{d.name} {len(d.d.keys())}")
2151 #@+node:ekr.20120214165710.10822: *4* LM.traceShortcutsDict
2152 def traceShortcutsDict(self, d, verbose=False):
2153 if verbose:
2154 print(d)
2155 for key in sorted(list(d.keys())):
2156 val = d.get(key)
2157 # print('%20s %s' % (key,val.dump()))
2158 print(f"{key:35} {[z.stroke for z in val]}")
2159 if d:
2160 print('')
2161 else:
2162 print(d)
2163 #@+node:ekr.20120219154958.10452: *3* LM.load & helpers
2164 def load(self, fileName=None, pymacs=None):
2165 """This is Leo's main startup method."""
2166 lm = self
2167 #
2168 # Phase 1: before loading plugins.
2169 # Scan options, set directories and read settings.
2170 t1 = time.process_time()
2171 print('') # Give some separation for the coming traces.
2172 if not lm.isValidPython():
2173 return
2174 lm.doPrePluginsInit(fileName, pymacs)
2175 # sets lm.options and lm.files
2176 g.app.computeSignon()
2177 g.app.printSignon()
2178 if lm.options.get('version'):
2179 return
2180 if not g.app.gui:
2181 return
2182 # Disable redraw until all files are loaded.
2183 g.app.disable_redraw = True
2184 #
2185 # Phase 2: load plugins: the gui has already been set.
2186 t2 = time.process_time()
2187 g.doHook("start1")
2188 t3 = time.process_time()
2189 if g.app.killed:
2190 return
2191 g.app.idleTimeManager.start()
2192 #
2193 # Phase 3: after loading plugins. Create one or more frames.
2194 t3 = time.process_time()
2195 if lm.options.get('script') and not self.files:
2196 ok = True
2197 else:
2198 ok = lm.doPostPluginsInit()
2199 # Fix #579: Key bindings don't take for commands defined in plugins
2200 g.app.makeAllBindings()
2201 if ok and g.app.diff:
2202 lm.doDiff()
2203 if not ok:
2204 return
2205 g.es('') # Clears horizontal scrolling in the log pane.
2206 if g.app.listen_to_log_flag:
2207 g.app.listenToLog()
2208 if 'startup' in g.app.debug:
2209 t4 = time.process_time()
2210 print('')
2211 g.es_print(f"settings:{t2 - t1:5.2f} sec")
2212 g.es_print(f" plugins:{t3 - t2:5.2f} sec")
2213 g.es_print(f" files:{t4 - t3:5.2f} sec")
2214 g.es_print(f" total:{t4 - t1:5.2f} sec")
2215 print('')
2216 # -- quit
2217 if g.app.quit_after_load:
2218 if 'shutdown' in g.app.debug or 'startup' in g.app.debug:
2219 print('--quit')
2220 g.app.forceShutdown()
2221 return
2222 # #1128: support for restart-leo.
2223 if not g.app.start_minimized:
2224 try: # Careful: we may be unit testing.
2225 g.app.log.c.frame.bringToFront()
2226 except Exception:
2227 pass
2228 g.app.gui.runMainLoop()
2229 # For scripts, the gui is a nullGui.
2230 # and the gui.setScript has already been called.
2231 #@+node:ekr.20150225133846.7: *4* LM.doDiff
2232 def doDiff(self):
2233 """Support --diff option after loading Leo."""
2234 if len(self.old_argv[2:]) == 2:
2235 pass # c.editFileCommands.compareAnyTwoFiles gives a message.
2236 else:
2237 # This is an unusual situation.
2238 g.es('--diff mode. sys.argv[2:]...', color='red')
2239 for z in self.old_argv[2:]:
2240 g.es(g.shortFileName(z) if z else repr(z), color='blue')
2241 commanders = g.app.commanders()
2242 if len(commanders) == 2:
2243 c = commanders[0]
2244 c.editFileCommands.compareAnyTwoFiles(event=None)
2245 #@+node:ekr.20120219154958.10487: *4* LM.doPostPluginsInit & helpers
2246 def doPostPluginsInit(self):
2247 """Create a Leo window for each file in the lm.files list."""
2248 # Clear g.app.initing _before_ creating commanders.
2249 lm = self
2250 g.app.initing = False # "idle" hooks may now call g.app.forceShutdown.
2251 # Create the main frame.Show it and all queued messages.
2252 c = c1 = fn = None
2253 if lm.files:
2254 try: # #1403.
2255 for n, fn in enumerate(lm.files):
2256 lm.more_cmdline_files = n < len(lm.files) - 1
2257 # Returns None if the file is open in another instance of Leo.
2258 c = lm.loadLocalFile(fn, gui=g.app.gui, old_c=None)
2259 if c and not c1: # #1416:
2260 c1 = c
2261 except Exception:
2262 g.es_print(f"Unexpected exception reading {fn!r}")
2263 g.es_exception()
2264 c = None
2265 # Load (and save later) a session *only* if the command line contains no files.
2266 g.app.loaded_session = not lm.files
2267 if g.app.sessionManager and g.app.loaded_session:
2268 try: # #1403.
2269 aList = g.app.sessionManager.load_snapshot()
2270 if aList:
2271 g.app.sessionManager.load_session(c1, aList)
2272 # #659.
2273 if g.app.windowList:
2274 c = c1 = g.app.windowList[0].c
2275 else:
2276 c = c1 = None
2277 except Exception:
2278 g.es_print('Can not load session')
2279 g.es_exception()
2280 # Enable redraws.
2281 g.app.disable_redraw = False
2282 if not c1:
2283 try: # #1403.
2284 c1 = lm.openEmptyWorkBook() # Calls LM.loadLocalFile.
2285 except Exception:
2286 g.es_print('Can not create empty workbook')
2287 g.es_exception()
2288 c = c1
2289 if not c:
2290 # Leo is out of options: Force an immediate exit.
2291 return False
2292 # #199.
2293 g.app.runAlreadyOpenDialog(c1)
2294 #
2295 # Final inits...
2296 # For qt gui, select the first-loaded tab.
2297 if hasattr(g.app.gui, 'frameFactory'):
2298 factory = g.app.gui.frameFactory
2299 if factory and hasattr(factory, 'setTabForCommander'):
2300 factory.setTabForCommander(c)
2301 g.app.logInited = True
2302 g.app.initComplete = True
2303 c.setLog()
2304 c.redraw()
2305 g.doHook("start2", c=c, p=c.p, fileName=c.fileName())
2306 c.initialFocusHelper()
2307 screenshot_fn = lm.options.get('screenshot_fn')
2308 if screenshot_fn:
2309 lm.make_screen_shot(screenshot_fn)
2310 return False # Force an immediate exit.
2311 return True
2312 #@+node:ekr.20120219154958.10489: *5* LM.make_screen_shot
2313 def make_screen_shot(self, fn):
2314 """Create a screenshot of the present Leo outline and save it to path."""
2315 if g.app.gui.guiName() == 'qt':
2316 m = g.loadOnePlugin('screenshots')
2317 m.make_screen_shot(fn)
2318 #@+node:ekr.20131028155339.17098: *5* LM.openEmptyWorkBook
2319 def openEmptyWorkBook(self):
2320 """Open an empty frame and paste the contents of CheatSheet.leo into it."""
2321 lm = self
2322 # Create an empty frame.
2323 fn = lm.computeWorkbookFileName()
2324 if not fn:
2325 return None # #1415
2326 c = lm.loadLocalFile(fn, gui=g.app.gui, old_c=None)
2327 if not c:
2328 return None # #1201: AttributeError below.
2329 if g.app.batchMode or g.os_path_exists(fn):
2330 return c
2331 # Open the cheatsheet.
2332 fn = g.os_path_finalize_join(g.app.loadDir, '..', 'doc', 'CheatSheet.leo')
2333 if not g.os_path_exists(fn):
2334 g.es(f"file not found: {fn}")
2335 return None
2336 # Paste the contents of CheetSheet.leo into c.
2337 old_clipboard = g.app.gui.getTextFromClipboard() # #933: Save clipboard.
2338 c2 = g.openWithFileName(fn, old_c=c)
2339 for p2 in c2.rootPosition().self_and_siblings():
2340 c2.setCurrentPosition(p2) # 1380
2341 c2.copyOutline()
2342 # #1380 & #1381: Add guard & use vnode methods to prevent redraw.
2343 p = c.pasteOutline()
2344 if p:
2345 c.setCurrentPosition(p) # 1380
2346 p.v.contract()
2347 p.v.clearDirty()
2348 c2.close(new_c=c)
2349 # Delete the dummy first node.
2350 root = c.rootPosition()
2351 root.doDelete(newNode=root.next())
2352 c.target_language = 'rest'
2353 c.clearChanged()
2354 c.redraw(c.rootPosition()) # # 1380: Select the root.
2355 g.app.gui.replaceClipboardWith(old_clipboard) # #933: Restore clipboard
2356 return c
2357 #@+node:ekr.20120219154958.10477: *4* LM.doPrePluginsInit & helpers
2358 def doPrePluginsInit(self, fileName, pymacs):
2359 """ Scan options, set directories and read settings."""
2360 lm = self
2361 lm.computeStandardDirectories()
2362 # Scan the options as early as possible.
2363 lm.options = options = lm.scanOptions(fileName, pymacs) # also sets lm.files.
2364 if options.get('version'):
2365 return
2366 script = options.get('script')
2367 verbose = script is None
2368 # Init the app.
2369 lm.initApp(verbose)
2370 g.app.setGlobalDb()
2371 if verbose:
2372 lm.reportDirectories()
2373 # Read settings *after* setting g.app.config and *before* opening plugins.
2374 # This means if-gui has effect only in per-file settings.
2375 if g.app.quit_after_load:
2376 localConfigFile = None
2377 else:
2378 # Read only standard settings files, using a null gui.
2379 # uses lm.files[0] to compute the local directory
2380 # that might contain myLeoSettings.leo.
2381 lm.readGlobalSettingsFiles()
2382 # Read the recent files file.
2383 localConfigFile = lm.files[0] if lm.files else None
2384 g.app.recentFilesManager.readRecentFiles(localConfigFile)
2385 # Create the gui after reading options and settings.
2386 lm.createGui(pymacs)
2387 # We can't print the signon until we know the gui.
2388 g.app.computeSignon() # Set app.signon/signon1 for commanders.
2389 #@+node:ekr.20170302093006.1: *5* LM.createAllImporterData & helpers
2390 def createAllImporterData(self):
2391 """
2392 New in Leo 5.5:
2394 Create global data structures describing importers and writers.
2395 """
2396 assert g.app.loadDir
2397 # This is the only data required.
2398 self.createWritersData()
2399 # Was an AtFile method.
2400 self.createImporterData()
2401 # Was a LeoImportCommands method.
2402 #@+node:ekr.20140724064952.18037: *6* LM.createImporterData & helper
2403 def createImporterData(self):
2404 """Create the data structures describing importer plugins."""
2405 # Allow plugins to be defined in ~/.leo/plugins.
2406 plugins1 = g.os_path_finalize_join(g.app.homeDir, '.leo', 'plugins')
2407 plugins2 = g.os_path_finalize_join(g.app.loadDir, '..', 'plugins')
2408 for kind, plugins in (('home', plugins1), ('leo', plugins2)):
2409 pattern = g.os_path_finalize_join(
2410 g.app.loadDir, '..', 'plugins', 'importers', '*.py')
2411 for fn in g.glob_glob(pattern):
2412 sfn = g.shortFileName(fn)
2413 if sfn != '__init__.py':
2414 try:
2415 module_name = sfn[:-3]
2416 # Important: use importlib to give imported modules
2417 # their fully qualified names.
2418 m = importlib.import_module(
2419 f"leo.plugins.importers.{module_name}")
2420 self.parse_importer_dict(sfn, m)
2421 # print('createImporterData', m.__name__)
2422 except Exception:
2423 g.warning(f"can not import leo.plugins.importers.{module_name}")
2424 #@+node:ekr.20140723140445.18076: *7* LM.parse_importer_dict
2425 def parse_importer_dict(self, sfn, m):
2426 """
2427 Set entries in g.app.classDispatchDict, g.app.atAutoDict and
2428 g.app.atAutoNames using entries in m.importer_dict.
2429 """
2430 importer_d = getattr(m, 'importer_dict', None)
2431 if importer_d:
2432 at_auto = importer_d.get('@auto', [])
2433 scanner_func = importer_d.get('func', None)
2434 # scanner_name = scanner_class.__name__
2435 extensions = importer_d.get('extensions', [])
2436 if at_auto:
2437 # Make entries for each @auto type.
2438 d = g.app.atAutoDict
2439 for s in at_auto:
2440 d[s] = scanner_func
2441 g.app.atAutoDict[s] = scanner_func
2442 g.app.atAutoNames.add(s)
2443 if extensions:
2444 # Make entries for each extension.
2445 d = g.app.classDispatchDict
2446 for ext in extensions:
2447 d[ext] = scanner_func #importer_d.get('func')#scanner_class
2448 elif sfn not in (
2449 # These are base classes, not real plugins.
2450 'basescanner.py',
2451 'linescanner.py',
2452 ):
2453 g.warning(f"leo/plugins/importers/{sfn} has no importer_dict")
2454 #@+node:ekr.20140728040812.17990: *6* LM.createWritersData & helper
2455 def createWritersData(self):
2456 """Create the data structures describing writer plugins."""
2457 # Do *not* remove this trace.
2458 trace = False and 'createWritersData' not in g.app.debug_dict
2459 if trace:
2460 # Suppress multiple traces.
2461 g.app.debug_dict['createWritersData'] = True
2462 g.app.writersDispatchDict = {}
2463 g.app.atAutoWritersDict = {}
2464 plugins1 = g.os_path_finalize_join(g.app.homeDir, '.leo', 'plugins')
2465 plugins2 = g.os_path_finalize_join(g.app.loadDir, '..', 'plugins')
2466 for kind, plugins in (('home', plugins1), ('leo', plugins2)):
2467 pattern = g.os_path_finalize_join(g.app.loadDir,
2468 '..', 'plugins', 'writers', '*.py')
2469 for fn in g.glob_glob(pattern):
2470 sfn = g.shortFileName(fn)
2471 if sfn != '__init__.py':
2472 try:
2473 # Important: use importlib to give imported modules their fully qualified names.
2474 m = importlib.import_module(f"leo.plugins.writers.{sfn[:-3]}")
2475 self.parse_writer_dict(sfn, m)
2476 except Exception:
2477 g.es_exception()
2478 g.warning(f"can not import leo.plugins.writers.{sfn}")
2479 if trace:
2480 g.trace('LM.writersDispatchDict')
2481 g.printDict(g.app.writersDispatchDict)
2482 g.trace('LM.atAutoWritersDict')
2483 g.printDict(g.app.atAutoWritersDict)
2484 #@+node:ekr.20140728040812.17991: *7* LM.parse_writer_dict
2485 def parse_writer_dict(self, sfn, m):
2486 """
2487 Set entries in g.app.writersDispatchDict and g.app.atAutoWritersDict
2488 using entries in m.writers_dict.
2489 """
2490 writer_d = getattr(m, 'writer_dict', None)
2491 if writer_d:
2492 at_auto = writer_d.get('@auto', [])
2493 scanner_class = writer_d.get('class', None)
2494 extensions = writer_d.get('extensions', [])
2495 if at_auto:
2496 # Make entries for each @auto type.
2497 d = g.app.atAutoWritersDict
2498 for s in at_auto:
2499 aClass = d.get(s)
2500 if aClass and aClass != scanner_class:
2501 g.trace(
2502 f"{sfn}: duplicate {s} class {aClass.__name__} "
2503 f"in {m.__file__}:")
2504 else:
2505 d[s] = scanner_class
2506 g.app.atAutoNames.add(s)
2507 if extensions:
2508 # Make entries for each extension.
2509 d = g.app.writersDispatchDict
2510 for ext in extensions:
2511 aClass = d.get(ext)
2512 if aClass and aClass != scanner_class:
2513 g.trace(f"{sfn}: duplicate {ext} class", aClass, scanner_class)
2514 else:
2515 d[ext] = scanner_class
2516 elif sfn not in ('basewriter.py',):
2517 g.warning(f"leo/plugins/writers/{sfn} has no writer_dict")
2518 #@+node:ekr.20120219154958.10478: *5* LM.createGui
2519 def createGui(self, pymacs):
2520 lm = self
2521 gui_option = lm.options.get('gui')
2522 windowFlag = lm.options.get('windowFlag')
2523 script = lm.options.get('script')
2524 if g.app.gui:
2525 if g.app.gui == g.app.nullGui:
2526 g.app.gui = None # Enable g.app.createDefaultGui
2527 g.app.createDefaultGui(__file__)
2528 else:
2529 pass
2530 # This can happen when launching Leo from IPython.
2531 # This can also happen when leoID does not exist.
2532 elif gui_option is None:
2533 if script and not windowFlag:
2534 # Always use null gui for scripts.
2535 g.app.createNullGuiWithScript(script)
2536 else:
2537 g.app.createDefaultGui(__file__)
2538 else:
2539 lm.createSpecialGui(gui_option, pymacs, script, windowFlag)
2540 #@+node:ekr.20120219154958.10479: *5* LM.createSpecialGui
2541 def createSpecialGui(self, gui, pymacs, script, windowFlag):
2542 # lm = self
2543 if pymacs:
2544 g.app.createNullGuiWithScript(script=None)
2545 elif script:
2546 if windowFlag:
2547 g.app.createDefaultGui()
2548 g.app.gui.setScript(script=script)
2549 sys.argv = [] # 2021/06/24: corrected by mypy.
2550 else:
2551 g.app.createNullGuiWithScript(script=script)
2552 else:
2553 g.app.createDefaultGui()
2554 #@+node:ekr.20120219154958.10482: *5* LM.getDefaultFile
2555 def getDefaultFile(self):
2556 # Get the name of the workbook.
2557 fn = g.app.config.getString('default-leo-file')
2558 fn = g.os_path_finalize(fn)
2559 if not fn:
2560 return None
2561 if g.os_path_exists(fn):
2562 return fn
2563 if g.os_path_isabs(fn):
2564 # Create the file.
2565 g.error(f"Using default leo file name:\n{fn}")
2566 return fn
2567 # It's too risky to open a default file if it is relative.
2568 return None
2569 #@+node:ekr.20120219154958.10484: *5* LM.initApp
2570 def initApp(self, verbose):
2572 self.createAllImporterData()
2573 # Can be done early. Uses only g.app.loadDir & g.app.homeDir.
2574 assert g.app.loadManager
2575 from leo.core import leoBackground
2576 from leo.core import leoConfig
2577 from leo.core import leoNodes
2578 from leo.core import leoPlugins
2579 from leo.core import leoSessions
2580 # Import leoIPython only if requested. The import is quite slow.
2581 self.setStdStreams()
2582 if g.app.useIpython:
2583 from leo.core import leoIPython
2584 # This launches the IPython Qt Console. It *is* required.
2585 assert leoIPython # suppress pyflakes/flake8 warning.
2586 # Make sure we call the new leoPlugins.init top-level function.
2587 leoPlugins.init()
2588 # Force the user to set g.app.leoID.
2589 g.app.setLeoID(verbose=verbose)
2590 # Create early classes *after* doing plugins.init()
2591 g.app.idleTimeManager = IdleTimeManager()
2592 g.app.backgroundProcessManager = leoBackground.BackgroundProcessManager()
2593 g.app.externalFilesController = leoExternalFiles.ExternalFilesController()
2594 g.app.recentFilesManager = RecentFilesManager()
2595 g.app.config = leoConfig.GlobalConfigManager()
2596 g.app.nodeIndices = leoNodes.NodeIndices(g.app.leoID)
2597 g.app.sessionManager = leoSessions.SessionManager()
2598 # Complete the plugins class last.
2599 g.app.pluginsController.finishCreate()
2600 #@+node:ekr.20210927034148.1: *5* LM.scanOptions & helpers
2601 def scanOptions(self, fileName, pymacs):
2602 """Handle all options, remove them from sys.argv and set lm.options."""
2603 lm = self
2604 table = (
2605 '--dock',
2606 '--global-docks', # #1643. use --use-docks instead.
2607 '--init-docks',
2608 '--no-cache',
2609 '--no-dock', # #1171 and #1514: use --use-docks instead.
2610 '--session-restore',
2611 '--session-save',
2612 '--use-docks',
2613 )
2614 trace_m = textwrap.dedent("""\
2615 abbrev, beauty, cache, coloring, drawing, events, focus, git, gnx
2616 importers, ipython, keys, layouts, plugins, save, select, sections,
2617 shutdown, size, speed, startup, themes, undo, verbose, zoom
2618 """)
2619 for bad_option in table:
2620 if bad_option in sys.argv:
2621 sys.argv.remove(bad_option)
2622 print(f"Ignoring the unused/deprecated {bad_option} option")
2623 lm.old_argv = sys.argv[:]
2624 # Automatically implements the --help option.
2625 description = "usage: launchLeo.py [options] file1, file2, ..."
2626 parser = argparse.ArgumentParser(
2627 description=description,
2628 formatter_class=argparse.RawTextHelpFormatter)
2629 #
2630 # Parse the options, and remove them from sys.argv.
2631 self.addOptionsToParser(parser, trace_m)
2632 args = parser.parse_args()
2633 # Handle simple args...
2634 self.doSimpleOptions(args, trace_m)
2635 # Compute the lm.files ivar.
2636 lm.files = lm.computeFilesList(fileName)
2637 # Compute the script. Used twice below.
2638 script = None if pymacs else self.doScriptOption(args, parser)
2639 # Return the dictionary of options.
2640 return {
2641 'gui': lm.doGuiOption(args),
2642 'load_type': lm.doLoadTypeOption(args),
2643 'screenshot_fn': lm.doScreenShotOption(args), # --screen-shot=fn
2644 'script': script,
2645 'select': args.select and args.select.strip('"'),
2646 # --select=headline
2647 'theme_path': args.theme, # --theme=name
2648 'version': args.version, # --version: print the version and exit.
2649 'windowFlag': script and args.script_window,
2650 'windowSize': lm.doWindowSizeOption(args),
2651 'windowSpot': lm.doWindowSpotOption(args),
2652 }
2653 #@+node:ekr.20210927034148.2: *6* LM.addOptionsToParser
2654 #@@nobeautify
2656 def addOptionsToParser(self, parser, trace_m):
2657 """Init the argsparse parser."""
2658 add = parser.add_argument
2659 add('PATHS', nargs='*', metavar='FILES',
2660 help='list of files')
2661 add('--diff', dest='diff', action='store_true',
2662 help='use Leo as an external git diff')
2663 add('--fail-fast', dest='fail_fast', action='store_true',
2664 help='stop unit tests after the first failure')
2665 add('--fullscreen', dest='fullscreen', action='store_true',
2666 help='start fullscreen')
2667 add('--ipython', dest='ipython', action='store_true',
2668 help='enable ipython support')
2669 add('--gui', dest='gui', metavar='GUI',
2670 help='gui to use (qt/console/null)')
2671 add('--listen-to-log', dest='listen_to_log', action='store_true',
2672 help='start log_listener.py on startup')
2673 add('--load-type', dest='load_type', metavar='TYPE',
2674 help='@<file> type for non-outlines')
2675 add('--maximized', dest='maximized', action='store_true',
2676 help='start maximized')
2677 add('--minimized', dest='minimized', action='store_true',
2678 help='start minimized')
2679 add('--no-plugins', dest='no_plugins', action='store_true',
2680 help='disable all plugins')
2681 add('--no-splash', dest='no_splash', action='store_true',
2682 help='disable the splash screen')
2683 add('--quit', dest='quit', action='store_true',
2684 help='quit immediately after loading')
2685 add('--screen-shot', dest='screen_shot', metavar='PATH',
2686 help='take a screen shot and then exit')
2687 add('--script', dest='script', metavar="PATH",
2688 help='execute a script and then exit')
2689 add('--script-window', dest='script_window', action='store_true',
2690 help='execute script using default gui')
2691 add('--select', dest='select', metavar='ID',
2692 help='headline or gnx of node to select')
2693 add('--silent', dest='silent', action='store_true',
2694 help='disable all log messages')
2695 add('--theme', dest='theme', metavar='NAME',
2696 help='use the named theme file')
2697 add('--trace', dest='trace', metavar='TRACE-KEY',
2698 help=f"add one or more strings to g.app.debug. One or more of...\n{trace_m}")
2699 add('--trace-binding', dest='trace_binding', metavar='KEY',
2700 help='trace commands bound to a key')
2701 add('--trace-setting', dest='trace_setting', metavar="NAME",
2702 help='trace where named setting is set')
2703 add('--window-size', dest='window_size', metavar='SIZE',
2704 help='initial window size (height x width)')
2705 add('--window-spot', dest='window_spot', metavar='SPOT',
2706 help='initial window position (top x left)')
2707 add('-v', '--version', dest='version', action='store_true',
2708 help='print version number and exit')
2709 #@+node:ekr.20210927034148.3: *6* LM.computeFilesList
2710 def computeFilesList(self, fileName):
2711 """Return the list of files on the command line."""
2712 lm = self
2713 files = []
2714 if fileName:
2715 files.append(fileName)
2716 for arg in sys.argv[1:]:
2717 if arg and not arg.startswith('-'):
2718 files.append(arg)
2719 result = []
2720 for z in files:
2721 # Fix #245: wrong: result.extend(glob.glob(lm.completeFileName(z)))
2722 aList = g.glob_glob(lm.completeFileName(z))
2723 if aList:
2724 result.extend(aList)
2725 else:
2726 result.append(z)
2727 return [g.os_path_normslashes(z) for z in result]
2728 #@+node:ekr.20210927034148.4: *6* LM.doGuiOption
2729 def doGuiOption(self, args):
2730 gui = args.gui
2731 if gui:
2732 gui = gui.lower()
2733 if gui in ('qt', 'qttabs'):
2734 gui = 'qt' # Allow qttabs gui.
2735 elif gui.startswith('browser'):
2736 pass
2737 elif gui in ('console', 'curses', 'text', 'null'):
2738 pass
2739 else:
2740 print(f"scanOptions: unknown gui: {gui}. Using qt gui")
2741 gui = 'qt'
2742 else:
2743 gui = 'qt'
2744 assert gui
2745 g.app.guiArgName = gui
2746 return gui
2747 #@+node:ekr.20210927034148.5: *6* LM.doLoadTypeOption
2748 def doLoadTypeOption(self, args):
2750 s = args.load_type
2751 s = s.lower() if s else 'edit'
2752 return '@' + s
2753 #@+node:ekr.20210927034148.6: *6* LM.doScreenShotOption
2754 def doScreenShotOption(self, args):
2756 # --screen-shot=fn
2757 s = args.screen_shot
2758 if s:
2759 s = s.strip('"')
2760 return s
2761 #@+node:ekr.20210927034148.7: *6* LM.doScriptOption
2762 def doScriptOption(self, args, parser):
2764 # --script
2765 script = args.script
2766 if script:
2767 # #1090: use cwd, not g.app.loadDir, to find scripts.
2768 fn = g.os_path_finalize_join(os.getcwd(), script)
2769 script, e = g.readFileIntoString(fn, kind='script:', verbose=False)
2770 if not script:
2771 print(f"script not found: {fn}")
2772 sys.exit(1)
2773 else:
2774 script = None
2775 return script
2776 #@+node:ekr.20210927034148.8: *6* LM.doSimpleOptions
2777 def doSimpleOptions(self, args, trace_m):
2778 """These args just set g.app ivars."""
2779 # --fail-fast
2780 g.app.failFast = args.fail_fast
2781 # --fullscreen
2782 g.app.start_fullscreen = args.fullscreen
2783 # --git-diff
2784 g.app.diff = args.diff
2785 # --listen-to-log
2786 g.app.listen_to_log_flag = args.listen_to_log
2787 # --ipython
2788 g.app.useIpython = args.ipython
2789 # --maximized
2790 g.app.start_maximized = args.maximized
2791 # --minimized
2792 g.app.start_minimized = args.minimized
2793 # --no-plugins
2794 if args.no_plugins:
2795 g.app.enablePlugins = False
2796 # --no-splash: --minimized disables the splash screen
2797 g.app.use_splash_screen = not args.no_splash and not args.minimized
2798 # -- quit
2799 g.app.quit_after_load = args.quit
2800 # --silent
2801 g.app.silentMode = args.silent
2802 # --trace=...
2803 valid = trace_m.replace(' ', '').replace('\n', '').split(',')
2804 if args.trace:
2805 ok = True
2806 values = args.trace.lstrip('(').lstrip('[').rstrip(')').rstrip(']')
2807 for val in values.split(','):
2808 if val in valid:
2809 g.app.debug.append(val)
2810 else:
2811 g.es_print(f"unknown --trace value: {val}")
2812 ok = False
2813 if not ok:
2814 g.es_print('Valid --trace values are...')
2815 for line in trace_m.split('\n'):
2816 print(' ', line.rstrip())
2817 #
2818 # These are not bool args.
2819 # --trace-binding
2820 g.app.trace_binding = args.trace_binding # g.app.config does not exist yet.
2821 #
2822 # --trace-setting=setting
2823 g.app.trace_setting = args.trace_setting # g.app.config does not exist yet.
2824 #@+node:ekr.20210927034148.9: *6* LM.doWindowSpotOption
2825 def doWindowSpotOption(self, args):
2827 # --window-spot
2828 spot = args.window_spot
2829 if spot:
2830 try:
2831 top, left = spot.split('x')
2832 spot = int(top), int(left)
2833 except ValueError:
2834 print('scanOptions: bad --window-spot:', spot)
2835 spot = None
2837 return spot
2838 #@+node:ekr.20210927034148.10: *6* LM.doWindowSizeOption
2839 def doWindowSizeOption(self, args):
2841 # --window-size
2842 windowSize = args.window_size
2843 if windowSize:
2844 try:
2845 h, w = windowSize.split('x')
2846 windowSize = int(h), int(w)
2847 except ValueError:
2848 windowSize = None
2849 print('scanOptions: bad --window-size:', windowSize)
2850 return windowSize
2851 #@+node:ekr.20160718072648.1: *5* LM.setStdStreams
2852 def setStdStreams(self):
2853 """
2854 Make sure that stdout and stderr exist.
2855 This is an issue when running Leo with pythonw.exe.
2856 """
2857 # Define class LeoStdOut
2858 #@+others
2859 #@+node:ekr.20160718091844.1: *6* class LeoStdOut
2860 class LeoStdOut:
2861 """A class to put stderr & stdout to Leo's log pane."""
2863 def __init__(self, kind):
2864 self.kind = kind
2865 g.es_print = self.write
2866 g.pr = self.write
2868 def flush(self, *args, **keys):
2869 pass
2870 #@+others
2871 #@+node:ekr.20160718102306.1: *7* LeoStdOut.write
2872 def write(self, *args, **keys):
2873 """Put all non-keyword args to the log pane, as in g.es."""
2874 #
2875 # Tracing will lead to unbounded recursion unless
2876 # sys.stderr has been redirected on the command line.
2877 app = g.app
2878 if not app or app.killed:
2879 return
2880 if app.gui and app.gui.consoleOnly:
2881 return
2882 log = app.log
2883 # Compute the effective args.
2884 d = {
2885 'color': None,
2886 'commas': False,
2887 'newline': True,
2888 'spaces': True,
2889 'tabName': 'Log',
2890 }
2891 # Handle keywords for g.pr and g.es_print.
2892 d = g.doKeywordArgs(keys, d)
2893 color: Any = d.get('color')
2894 if color == 'suppress':
2895 return
2896 if log and color is None:
2897 color = g.actualColor('black')
2898 color = g.actualColor(color)
2899 tabName = d.get('tabName') or 'Log'
2900 newline = d.get('newline')
2901 s = g.translateArgs(args, d)
2902 if app.batchMode:
2903 if log:
2904 log.put(s)
2905 elif log and app.logInited:
2906 # from_redirect is the big difference between this and g.es.
2907 log.put(s, color=color, tabName=tabName, from_redirect=True)
2908 else:
2909 app.logWaiting.append((s, color, newline),)
2910 #@-others
2911 #@-others
2912 if not sys.stdout:
2913 sys.stdout = sys.__stdout__ = LeoStdOut('stdout') # type:ignore
2914 if not sys.stderr:
2915 sys.stderr = sys.__stderr__ = LeoStdOut('stderr') # type:ignore
2916 #@+node:ekr.20120219154958.10491: *4* LM.isValidPython
2917 def isValidPython(self):
2918 if sys.platform == 'cli':
2919 return True
2920 message = (
2921 f"Leo requires Python {g.minimum_python_version} or higher"
2922 f"You may download Python from http://python.org/download/")
2923 try:
2924 version = '.'.join([str(sys.version_info[i]) for i in (0, 1, 2)])
2925 ok = g.CheckVersion(version, g.minimum_python_version)
2926 if not ok:
2927 print(message)
2928 try:
2929 # g.app.gui does not exist yet.
2930 d = g.EmergencyDialog(
2931 title='Python Version Error',
2932 message=message)
2933 d.run()
2934 except Exception:
2935 g.es_exception()
2936 return ok
2937 except Exception:
2938 print("isValidPython: unexpected exception: g.CheckVersion")
2939 traceback.print_exc()
2940 return 0
2941 #@+node:ekr.20120223062418.10393: *4* LM.loadLocalFile & helpers
2942 def loadLocalFile(self, fn, gui, old_c):
2943 """Completely read a file, creating the corresonding outline.
2945 1. If fn is an existing .leo, .db or .leojs file, read it twice:
2946 the first time with a NullGui to discover settings,
2947 the second time with the requested gui to create the outline.
2949 2. If fn is an external file:
2950 get settings from the leoSettings.leo and myLeoSetting.leo, then
2951 create a "wrapper" outline continain an @file node for the external file.
2953 3. If fn is empty:
2954 get settings from the leoSettings.leo and myLeoSetting.leo or default settings,
2955 or open an empty outline.
2956 """
2957 lm = self
2958 # #2489: If fn is empty, open an empty, untitled .leo file.
2959 if not fn:
2960 return lm.openEmptyLeoFile(gui, old_c)
2961 # Return the commander if the file is an already open outline.
2962 fn = g.os_path_finalize(fn) # #2489.
2963 c = lm.findOpenFile(fn)
2964 if c:
2965 return c
2966 # Open the file, creating a wrapper .leo file if necessary.
2967 previousSettings = lm.getPreviousSettings(fn)
2968 c = lm.openFileByName(fn, gui, old_c, previousSettings)
2969 return c
2970 #@+node:ekr.20220318033804.1: *5* LM.openEmptyLeoFile
2971 def openEmptyLeoFile(self, gui, old_c):
2972 """Open an empty, untitled, new Leo file."""
2973 lm = self
2974 # Disable the log.
2975 g.app.setLog(None)
2976 g.app.lockLog()
2977 # Create the commander for the .leo file.
2978 c = g.app.newCommander(
2979 fileName=None,
2980 gui=gui,
2981 previousSettings=lm.getPreviousSettings(None),
2982 )
2983 g.doHook('open0')
2984 # Enable the log.
2985 g.app.unlockLog()
2986 c.frame.log.enable(True)
2987 g.doHook("open1", old_c=old_c, c=c, new_c=c, fileName=None)
2988 # Init the frame.
2989 c.frame.setInitialWindowGeometry()
2990 c.frame.deiconify()
2991 c.frame.lift()
2992 c.frame.splitVerticalFlag, r1, r2 = c.frame.initialRatios()
2993 c.frame.resizePanesToRatio(r1, r2)
2994 c.mFileName = None
2995 c.wrappedFileName = None
2996 c.frame.title = c.computeWindowTitle(c.mFileName)
2997 c.frame.setTitle(c.frame.title)
2998 # Late inits. Order matters.
2999 if c.config.getBool('use-chapters') and c.chapterController:
3000 c.chapterController.finishCreate()
3001 c.clearChanged()
3002 g.doHook("open2", old_c=old_c, c=c, new_c=c, fileName=None)
3003 g.doHook("new", old_c=old_c, c=c, new_c=c)
3004 g.app.writeWaitingLog(c)
3005 c.setLog()
3006 lm.createMenu(c)
3007 lm.finishOpen(c)
3008 return c
3009 #@+node:ekr.20120223062418.10394: *5* LM.openFileByName & helpers
3010 def openFileByName(self, fn, gui, old_c, previousSettings):
3011 """
3012 Create an outline (Commander) for either:
3013 - a Leo file (including .leo or zipped file),
3014 - an external file.
3016 Note: The settings don't matter for pre-reads!
3017 For second read, the settings for the file are *exactly* previousSettings.
3018 """
3019 lm = self
3020 # Disable the log.
3021 g.app.setLog(None)
3022 g.app.lockLog()
3023 # Create the a commander for the .leo file.
3024 c = g.app.newCommander(
3025 fileName=fn,
3026 gui=gui,
3027 previousSettings=previousSettings,
3028 )
3029 g.doHook('open0')
3030 # Open the file before the open1 hook.
3031 theFile = lm.openAnyLeoFile(fn)
3032 if isinstance(theFile, sqlite3.Connection):
3033 # This commander is associated with sqlite db.
3034 c.sqlite_connection = theFile
3035 # Enable the log and do the open1 hook.
3036 g.app.unlockLog()
3037 c.frame.log.enable(True)
3038 # Create the outline.
3039 g.doHook("open1", old_c=None, c=c, new_c=c, fileName=fn)
3040 if theFile:
3041 # Some kind of existing Leo file.
3042 readAtFileNodesFlag = bool(previousSettings)
3043 # Read the Leo file.
3044 ok = lm.readOpenedLeoFile(c, fn, readAtFileNodesFlag, theFile)
3045 if not ok:
3046 return None
3047 else:
3048 # Not a any kind of Leo file. Create a wrapper .leo file.
3049 c = lm.initWrapperLeoFile(c, fn) # #2489
3050 g.doHook("new", old_c=old_c, c=c, new_c=c) # #2489.
3051 g.doHook("open2", old_c=old_c, c=c, new_c=c, fileName=fn)
3052 # Complete the inits.
3053 g.app.writeWaitingLog(c)
3054 c.setLog()
3055 lm.createMenu(c, fn)
3056 lm.finishOpen(c)
3057 return c
3058 #@+node:ekr.20120223062418.10405: *6* LM.createMenu
3059 def createMenu(self, c, fn=None):
3060 # lm = self
3061 # Create the menu as late as possible so it can use user commands.
3062 if not g.doHook("menu1", c=c, p=c.p, v=c.p):
3063 c.frame.menu.createMenuBar(c.frame)
3064 g.app.recentFilesManager.updateRecentFiles(fn)
3065 g.doHook("menu2", c=c, p=c.p, v=c.p)
3066 g.doHook("after-create-leo-frame", c=c)
3067 g.doHook("after-create-leo-frame2", c=c)
3068 # Fix bug 844953: tell Unity which menu to use.
3069 # c.enableMenuBar()
3070 #@+node:ekr.20120223062418.10406: *6* LM.findOpenFile
3071 def findOpenFile(self, fn):
3073 def munge(name):
3074 return g.os_path_normpath(name or '').lower()
3076 for frame in g.app.windowList:
3077 c = frame.c
3078 if g.os_path_realpath(munge(fn)) == g.os_path_realpath(munge(c.mFileName)):
3079 # Don't call frame.bringToFront(), it breaks --minimize
3080 c.setLog()
3081 # Selecting the new tab ensures focus is set.
3082 master = getattr(frame.top, 'leo_master', None)
3083 if master: # master is a TabbedTopLevel.
3084 master.select(frame.c)
3085 c.outerUpdate()
3086 return c
3087 return None
3088 #@+node:ekr.20120223062418.10407: *6* LM.finishOpen
3089 def finishOpen(self, c):
3090 # lm = self
3091 k = c.k
3092 assert k
3093 # New in Leo 4.6: provide an official way for very late initialization.
3094 c.frame.tree.initAfterLoad()
3095 c.initAfterLoad()
3096 # chapterController.finishCreate must be called after the first real redraw
3097 # because it requires a valid value for c.rootPosition().
3098 if c.chapterController:
3099 c.chapterController.finishCreate()
3100 if k:
3101 k.setDefaultInputState()
3102 c.initialFocusHelper()
3103 if k:
3104 k.showStateAndMode()
3105 c.frame.initCompleteHint()
3106 c.outerUpdate() # #181: Honor focus requests.
3107 #@+node:ekr.20120223062418.10408: *6* LM.initWrapperLeoFile
3108 def initWrapperLeoFile(self, c, fn):
3109 """
3110 Create an empty file if the external fn is empty.
3112 Otherwise, create an @edit or @file node for the external file.
3113 """
3114 # lm = self
3115 # Use the config params to set the size and location of the window.
3116 frame = c.frame
3117 frame.setInitialWindowGeometry()
3118 frame.deiconify()
3119 frame.lift()
3120 # #1570: Resize the _new_ frame.
3121 frame.splitVerticalFlag, r1, r2 = frame.initialRatios()
3122 frame.resizePanesToRatio(r1, r2)
3123 if not g.os_path_exists(fn):
3124 p = c.rootPosition()
3125 # Create an empty @edit node unless fn is an .leo file.
3126 # Fix #1070: Use "newHeadline", not fn.
3127 p.h = "newHeadline" if fn.endswith('.leo') else f"@edit {fn}"
3128 c.selectPosition(p)
3129 elif c.looksLikeDerivedFile(fn):
3130 # 2011/10/10: Create an @file node.
3131 p = c.importCommands.importDerivedFiles(parent=c.rootPosition(),
3132 paths=[fn], command=None) # Not undoable.
3133 if p and p.hasBack():
3134 p.back().doDelete()
3135 p = c.rootPosition()
3136 if not p:
3137 return None
3138 else:
3139 # Create an @<file> node.
3140 p = c.rootPosition()
3141 if p:
3142 # The 'load_type' key may not exist when run from the bridge.
3143 load_type = self.options.get('load_type', '@edit') # #2489.
3144 p.setHeadString(f"{load_type} {fn}")
3145 c.refreshFromDisk()
3146 c.selectPosition(p)
3147 # Fix critical bug 1184855: data loss with command line 'leo somefile.ext'
3148 # Fix smallish bug 1226816 Command line "leo xxx.leo" creates file xxx.leo.leo.
3149 c.mFileName = fn if fn.endswith('.leo') else f"{fn}.leo"
3150 c.wrappedFileName = fn
3151 c.frame.title = c.computeWindowTitle(c.mFileName)
3152 c.frame.setTitle(c.frame.title)
3153 if c.config.getBool('use-chapters') and c.chapterController:
3154 c.chapterController.finishCreate()
3155 frame.c.clearChanged()
3156 return c
3157 #@+node:ekr.20120223062418.10419: *6* LM.isLeoFile & LM.isZippedFile
3158 def isLeoFile(self, fn):
3159 """
3160 Return True if fn is any kind of Leo file,
3161 including a zipped file or .leo, .db, or .leojs file.
3162 """
3163 if not fn:
3164 return False
3165 return zipfile.is_zipfile(fn) or fn.endswith(('.leo', 'db', '.leojs'))
3167 def isZippedFile(self, fn):
3168 """Return True if fn is a zipped file."""
3169 return fn and zipfile.is_zipfile(fn)
3170 #@+node:ekr.20120224161905.10030: *6* LM.openAnyLeoFile
3171 def openAnyLeoFile(self, fn):
3172 """Open a .leo, .leojs or .db file."""
3173 lm = self
3174 if fn.endswith('.db'):
3175 return sqlite3.connect(fn)
3176 if lm.isLeoFile(fn) and g.os_path_exists(fn):
3177 if lm.isZippedFile(fn):
3178 theFile = lm.openZipFile(fn)
3179 else:
3180 theFile = lm.openLeoFile(fn)
3181 else:
3182 theFile = None
3183 return theFile
3184 #@+node:ekr.20120223062418.10416: *6* LM.openLeoFile
3185 def openLeoFile(self, fn):
3186 """Open the file for reading."""
3187 try:
3188 theFile = open(fn, 'rb')
3189 return theFile
3190 except IOError:
3191 # Do not use string + here: it will fail for non-ascii strings!
3192 if not g.unitTesting:
3193 g.error("can not open:", fn)
3194 return None
3195 #@+node:ekr.20120223062418.10410: *6* LM.openZipFile
3196 def openZipFile(self, fn):
3197 """
3198 Open a zipped file for reading.
3199 Return a StringIO file if successful.
3200 """
3201 try:
3202 theFile = zipfile.ZipFile(fn, 'r')
3203 if not theFile:
3204 return None
3205 # Read the file into an StringIO file.
3206 aList = theFile.namelist()
3207 name = aList and len(aList) == 1 and aList[0]
3208 if not name:
3209 return None
3210 s = theFile.read(name)
3211 s2 = g.toUnicode(s, 'utf-8')
3212 return StringIO(s2)
3213 except IOError:
3214 # Do not use string + here: it will fail for non-ascii strings!
3215 if not g.unitTesting:
3216 g.error("can not open:", fn)
3217 return None
3218 #@+node:ekr.20120223062418.10412: *6* LM.readOpenedLeoFile
3219 def readOpenedLeoFile(self, c, fn, readAtFileNodesFlag, theFile):
3220 """
3221 Call c.fileCommands.openLeoFile to open some kind of Leo file.
3223 the_file: An open file, which is a StringIO file for zipped files.
3225 Note: g.app.log is not inited here.
3226 """
3227 # New in Leo 4.10: The open1 event does not allow an override of the init logic.
3228 assert theFile
3229 # Read and close the file.
3230 ok = c.fileCommands.openLeoFile(
3231 theFile, fn, readAtFileNodesFlag=readAtFileNodesFlag)
3232 if ok:
3233 if not c.openDirectory:
3234 theDir = g.os_path_finalize(g.os_path_dirname(fn)) # 1341
3235 c.openDirectory = c.frame.openDirectory = theDir
3236 else:
3237 # #970: Never close Leo here.
3238 g.app.closeLeoWindow(c.frame, finish_quit=False)
3239 return ok
3240 #@+node:ekr.20160430063406.1: *3* LM.revertCommander
3241 def revertCommander(self, c):
3242 """Revert c to the previously saved contents."""
3243 lm = self
3244 fn = c.mFileName
3245 # Re-read the file.
3246 theFile = lm.openAnyLeoFile(fn)
3247 if theFile:
3248 c.fileCommands.initIvars()
3249 c.fileCommands.getLeoFile(theFile, fn, checkOpenFiles=False)
3250 # Closes the file.
3251 #@-others
3252#@+node:ekr.20120223062418.10420: ** class PreviousSettings
3253class PreviousSettings:
3254 """
3255 A class holding the settings and shortcuts dictionaries
3256 that are computed in the first pass when loading local
3257 files and passed to the second pass.
3258 """
3260 def __init__(self, settingsDict, shortcutsDict):
3261 if not shortcutsDict or not settingsDict: # #1766: unit tests.
3262 lm = g.app.loadManager
3263 settingsDict, shortcutsDict = lm.createDefaultSettingsDicts()
3264 self.settingsDict = settingsDict
3265 self.shortcutsDict = shortcutsDict
3267 def __repr__(self):
3268 return (
3269 f"<PreviousSettings\n"
3270 f"{self.settingsDict}\n"
3271 f"{self.shortcutsDict}\n>")
3273 __str__ = __repr__
3274#@+node:ekr.20120225072226.10283: ** class RecentFilesManager
3275class RecentFilesManager:
3276 """A class to manipulate leoRecentFiles.txt."""
3278 def __init__(self):
3280 self.edit_headline = 'Recent files. Do not change this headline!'
3281 self.groupedMenus = [] # Set in rf.createRecentFilesMenuItems.
3282 self.recentFiles = [] # List of g.Bunches describing .leoRecentFiles.txt files.
3283 self.recentFilesMenuName = 'Recent Files' # May be changed later.
3284 self.recentFileMessageWritten = False # To suppress all but the first message.
3285 self.write_recent_files_as_needed = False # Will be set later.
3287 #@+others
3288 #@+node:ekr.20041201080436: *3* rf.appendToRecentFiles
3289 def appendToRecentFiles(self, files):
3290 rf = self
3291 files = [theFile.strip() for theFile in files]
3293 def munge(name):
3294 return g.os_path_normpath(name or '').lower()
3296 for name in files:
3297 # Remove all variants of name.
3298 for name2 in rf.recentFiles[:]:
3299 if munge(name) == munge(name2):
3300 rf.recentFiles.remove(name2)
3301 rf.recentFiles.append(name)
3302 #@+node:ekr.20120225072226.10289: *3* rf.cleanRecentFiles
3303 def cleanRecentFiles(self, c):
3304 """
3305 Remove items from the recent files list that no longer exist.
3307 This almost never does anything because Leo's startup logic removes
3308 nonexistent files from the recent files list.
3309 """
3310 result = [z for z in self.recentFiles if g.os_path_exists(z)]
3311 if result != self.recentFiles:
3312 for path in result:
3313 self.updateRecentFiles(path)
3314 self.writeRecentFilesFile(c)
3315 #@+node:ekr.20180212141017.1: *3* rf.demangleRecentFiles
3316 def demangleRecentFiles(self, c, data):
3317 """Rewrite recent files based on c.config.getData('path-demangle')"""
3318 changes = []
3319 replace = None
3320 for line in data:
3321 text = line.strip()
3322 if text.startswith('REPLACE: '):
3323 replace = text.split(None, 1)[1].strip()
3324 if text.startswith('WITH:') and replace is not None:
3325 with_ = text[5:].strip()
3326 changes.append((replace, with_))
3327 g.es(f"{replace} -> {with_}")
3328 orig = [z for z in self.recentFiles if z.startswith("/")]
3329 self.recentFiles = []
3330 for i in orig:
3331 t = i
3332 for change in changes:
3333 t = t.replace(*change)
3334 self.updateRecentFiles(t)
3335 self.writeRecentFilesFile(c)
3336 #@+node:ekr.20120225072226.10297: *3* rf.clearRecentFiles
3337 def clearRecentFiles(self, c):
3338 """Clear the recent files list, then add the present file."""
3339 rf = self
3340 menu, u = c.frame.menu, c.undoer
3341 bunch = u.beforeClearRecentFiles()
3342 recentFilesMenu = menu.getMenu(self.recentFilesMenuName)
3343 menu.deleteRecentFilesMenuItems(recentFilesMenu)
3344 rf.recentFiles = [c.fileName()]
3345 for frame in g.app.windowList:
3346 rf.createRecentFilesMenuItems(frame.c)
3347 u.afterClearRecentFiles(bunch)
3348 # Write the file immediately.
3349 rf.writeRecentFilesFile(c)
3350 #@+node:ekr.20120225072226.10301: *3* rf.createRecentFilesMenuItems
3351 def createRecentFilesMenuItems(self, c):
3352 rf = self
3353 menu = c.frame.menu
3354 recentFilesMenu = menu.getMenu(self.recentFilesMenuName)
3355 if not recentFilesMenu:
3356 return
3357 # Delete all previous entries.
3358 menu.deleteRecentFilesMenuItems(recentFilesMenu)
3359 # Create the permanent (static) menu entries.
3360 table = rf.getRecentFilesTable()
3361 menu.createMenuEntries(recentFilesMenu, table)
3362 # Create all the other entries (a maximum of 36).
3363 accel_ch = string.digits + string.ascii_uppercase # Not a unicode problem.
3364 i = 0
3365 n = len(accel_ch)
3366 # see if we're grouping when files occur in more than one place
3367 rf_group = c.config.getBool("recent-files-group")
3368 rf_always = c.config.getBool("recent-files-group-always")
3369 groupedEntries = rf_group or rf_always
3370 if groupedEntries: # if so, make dict of groups
3371 dirCount: Dict[str, Any] = {}
3372 for fileName in rf.getRecentFiles()[:n]:
3373 dirName, baseName = g.os_path_split(fileName)
3374 if baseName not in dirCount:
3375 dirCount[baseName] = {'dirs': [], 'entry': None}
3376 dirCount[baseName]['dirs'].append(dirName)
3377 for name in rf.getRecentFiles()[:n]:
3378 # pylint: disable=cell-var-from-loop
3379 if name.strip() == "":
3380 continue # happens with empty list/new file
3382 def recentFilesCallback(event=None, c=c, name=name):
3383 c.openRecentFile(fn=name)
3385 if groupedEntries:
3386 dirName, baseName = g.os_path_split(name)
3387 entry = dirCount[baseName]
3388 if len(entry['dirs']) > 1 or rf_always: # sub menus
3389 if entry['entry'] is None:
3390 entry['entry'] = menu.createNewMenu(baseName, "Recent Files...")
3391 # acts as a flag for the need to create the menu
3392 c.add_command(menu.getMenu(baseName), label=dirName,
3393 command=recentFilesCallback, underline=0)
3394 else: # single occurence, no submenu
3395 c.add_command(recentFilesMenu, label=baseName,
3396 command=recentFilesCallback, underline=0)
3397 else: # original behavior
3398 label = f"{accel_ch[i]} {g.computeWindowTitle(name)}"
3399 c.add_command(recentFilesMenu, label=label,
3400 command=recentFilesCallback, underline=0)
3401 i += 1
3402 if groupedEntries: # store so we can delete them later
3403 rf.groupedMenus = [z for z in dirCount
3404 if dirCount[z]['entry'] is not None]
3405 #@+node:vitalije.20170703115609.1: *3* rf.editRecentFiles
3406 def editRecentFiles(self, c):
3407 """
3408 Dump recentFiles into new node appended as lastTopLevel, selects it and
3409 request focus in body.
3411 NOTE: command write-edited-recent-files assume that headline of this
3412 node is not changed by user.
3413 """
3414 rf = self
3415 p1 = c.lastTopLevel().insertAfter()
3416 p1.h = self.edit_headline
3417 p1.b = '\n'.join(rf.recentFiles)
3418 c.redraw()
3419 c.selectPosition(p1)
3420 c.redraw()
3421 c.bodyWantsFocusNow()
3422 g.es('edit list and run write-rff to save recentFiles')
3423 #@+node:ekr.20120225072226.10286: *3* rf.getRecentFiles
3424 def getRecentFiles(self):
3425 # Fix #299: Leo loads a deleted file.
3426 self.recentFiles = [z for z in self.recentFiles
3427 if g.os_path_exists(z)]
3428 return self.recentFiles
3429 #@+node:ekr.20120225072226.10304: *3* rf.getRecentFilesTable
3430 def getRecentFilesTable(self):
3431 return (
3432 "*clear-recent-files",
3433 "*clean-recent-files",
3434 "*demangle-recent-files",
3435 "*sort-recent-files",
3436 ("-", None, None),
3437 )
3438 #@+node:ekr.20070224115832: *3* rf.readRecentFiles & helpers
3439 def readRecentFiles(self, localConfigFile):
3440 """Read all .leoRecentFiles.txt files."""
3441 # The order of files in this list affects the order of the recent files list.
3442 rf = self
3443 seen = []
3444 localConfigPath = g.os_path_dirname(localConfigFile)
3445 for path in (g.app.homeLeoDir, g.app.globalConfigDir, localConfigPath):
3446 if path:
3447 path = g.os_path_realpath(g.os_path_finalize(path))
3448 if path and path not in seen:
3449 ok = rf.readRecentFilesFile(path)
3450 if ok:
3451 seen.append(path)
3452 if not seen and rf.write_recent_files_as_needed:
3453 rf.createRecentFiles()
3454 #@+node:ekr.20061010121944: *4* rf.createRecentFiles
3455 def createRecentFiles(self):
3456 """
3457 Try to create .leoRecentFiles.txt, in the users home directory, or in
3458 Leo's config directory if that fails.
3459 """
3460 for theDir in (g.app.homeLeoDir, g.app.globalConfigDir):
3461 if theDir:
3462 fn = g.os_path_join(theDir, '.leoRecentFiles.txt')
3463 try:
3464 with open(fn, 'w'):
3465 g.red('created', fn)
3466 return
3467 except IOError:
3468 g.error('can not create', fn)
3469 g.es_exception()
3470 #@+node:ekr.20050424115658: *4* rf.readRecentFilesFile
3471 def readRecentFilesFile(self, path):
3473 fileName = g.os_path_join(path, '.leoRecentFiles.txt')
3474 if not g.os_path_exists(fileName):
3475 return False
3476 try:
3477 with io.open(fileName, encoding='utf-8', mode='r') as f:
3478 try: # Fix #471.
3479 lines = f.readlines()
3480 except Exception:
3481 lines = None
3482 except IOError:
3483 # The file exists, so FileNotFoundError is not possible.
3484 g.trace('can not open', fileName)
3485 return False
3486 if lines and self.sanitize(lines[0]) == 'readonly':
3487 lines = lines[1:]
3488 if lines:
3489 lines = [g.toUnicode(g.os_path_normpath(line)) for line in lines]
3490 self.appendToRecentFiles(lines)
3491 return True
3492 #@+node:ekr.20120225072226.10285: *3* rf.sanitize
3493 def sanitize(self, name):
3494 """Return a sanitized file name."""
3495 if name is None:
3496 return None
3497 name = name.lower()
3498 for ch in ('-', '_', ' ', '\n'):
3499 name = name.replace(ch, '')
3500 return name or None
3501 #@+node:ekr.20120215072959.12478: *3* rf.setRecentFiles
3502 def setRecentFiles(self, files):
3503 """Update the recent files list."""
3504 rf = self
3505 rf.appendToRecentFiles(files)
3506 #@+node:ekr.20120225072226.10293: *3* rf.sortRecentFiles
3507 def sortRecentFiles(self, c):
3508 """Sort the recent files list."""
3509 rf = self
3511 def key(path):
3512 # Sort only the base name. That's what will appear in the menu.
3513 s = g.os_path_basename(path)
3514 return s.lower() if sys.platform.lower().startswith('win') else s
3516 aList = sorted(rf.recentFiles, key=key)
3517 rf.recentFiles = []
3518 for z in reversed(aList):
3519 rf.updateRecentFiles(z)
3520 rf.writeRecentFilesFile(c)
3521 #@+node:ekr.20031218072017.2083: *3* rf.updateRecentFiles
3522 def updateRecentFiles(self, fileName):
3523 """Create the RecentFiles menu. May be called with Null fileName."""
3524 rf = self
3525 if g.unitTesting:
3526 return
3528 def munge(name):
3529 return g.os_path_finalize(name or '').lower()
3531 def munge2(name):
3532 return g.os_path_finalize_join(g.app.loadDir, name or '')
3534 # Update the recent files list in all windows.
3536 if fileName:
3537 for frame in g.app.windowList:
3538 # Remove all versions of the file name.
3539 for name in rf.recentFiles:
3540 if (
3541 munge(fileName) == munge(name) or
3542 munge2(fileName) == munge2(name)
3543 ):
3544 rf.recentFiles.remove(name)
3545 rf.recentFiles.insert(0, fileName)
3546 # Recreate the Recent Files menu.
3547 rf.createRecentFilesMenuItems(frame.c)
3548 else:
3549 for frame in g.app.windowList:
3550 rf.createRecentFilesMenuItems(frame.c)
3551 #@+node:vitalije.20170703115616.1: *3* rf.writeEditedRecentFiles
3552 def writeEditedRecentFiles(self, c):
3553 """
3554 Write content of "edit_headline" node as recentFiles and recreates
3555 menues.
3556 """
3557 p, rf = c.p, self
3558 p = g.findNodeAnywhere(c, self.edit_headline)
3559 if p:
3560 files = [z for z in p.b.splitlines() if z and g.os_path_exists(z)]
3561 rf.recentFiles = files
3562 rf.writeRecentFilesFile(c)
3563 rf.updateRecentFiles(None)
3564 c.selectPosition(p)
3565 c.deleteOutline()
3566 else:
3567 g.red('not found:', self.edit_headline)
3568 #@+node:ekr.20050424114937.2: *3* rf.writeRecentFilesFile & helper
3569 def writeRecentFilesFile(self, c):
3570 """Write the appropriate .leoRecentFiles.txt file."""
3571 tag = '.leoRecentFiles.txt'
3572 rf = self
3573 # tag:#661. Do nothing if in leoBride.
3574 if g.unitTesting or g.app.inBridge:
3575 return
3576 localFileName = c.fileName()
3577 if localFileName:
3578 localPath, junk = g.os_path_split(localFileName)
3579 else:
3580 localPath = None
3581 written = False
3582 seen = []
3583 for path in (localPath, g.app.globalConfigDir, g.app.homeLeoDir):
3584 if path:
3585 fileName = g.os_path_join(path, tag)
3586 if g.os_path_exists(fileName) and fileName.lower() not in seen:
3587 seen.append(fileName.lower())
3588 ok = rf.writeRecentFilesFileHelper(fileName)
3589 if ok:
3590 written = True
3591 if not rf.recentFileMessageWritten and not g.unitTesting and not g.app.silentMode: # #459:
3592 if ok:
3593 g.es_print(f"wrote recent file: {fileName}")
3594 else:
3595 g.error(f"failed to write recent file: {fileName}")
3596 if written:
3597 rf.recentFileMessageWritten = True
3598 else:
3599 # Attempt to create .leoRecentFiles.txt in the user's home directory.
3600 if g.app.homeLeoDir:
3601 fileName = g.os_path_finalize_join(g.app.homeLeoDir, tag)
3602 if not g.os_path_exists(fileName):
3603 g.red(f"creating: {fileName}")
3604 rf.writeRecentFilesFileHelper(fileName)
3605 #@+node:ekr.20050424131051: *4* rf.writeRecentFilesFileHelper
3606 def writeRecentFilesFileHelper(self, fileName):
3607 # Don't update the file if it begins with read-only.
3608 #
3609 # Part 1: Return False if the first line is "readonly".
3610 # It's ok if the file doesn't exist.
3611 if g.os_path_exists(fileName):
3612 with io.open(fileName, encoding='utf-8', mode='r') as f:
3613 try:
3614 # Fix #471.
3615 lines = f.readlines()
3616 except Exception:
3617 lines = None
3618 if lines and self.sanitize(lines[0]) == 'readonly':
3619 return False
3620 # Part 2: write the files.
3621 try:
3622 with io.open(fileName, encoding='utf-8', mode='w') as f:
3623 s = '\n'.join(self.recentFiles) if self.recentFiles else '\n'
3624 f.write(g.toUnicode(s))
3625 return True
3626 except IOError:
3627 g.error('error writing', fileName)
3628 g.es_exception()
3629 except Exception:
3630 g.error('unexpected exception writing', fileName)
3631 g.es_exception()
3632 if g.unitTesting:
3633 raise
3634 return False
3635 #@-others
3636#@+node:ekr.20150514125218.1: ** Top-level-commands
3637#@+node:ekr.20150514125218.2: *3* ctrl-click-at-cursor
3638@g.command('ctrl-click-at-cursor')
3639def ctrlClickAtCursor(event):
3640 """Simulate a control-click at the cursor."""
3641 c = event.get('c')
3642 if c:
3643 g.openUrlOnClick(event)
3644#@+node:ekr.20180213045148.1: *3* demangle-recent-files
3645@g.command('demangle-recent-files')
3646def demangle_recent_files_command(event):
3647 """
3648 Path demangling potentially alters the paths in the recent files list
3649 according to find/replace patterns in the @data path-demangle setting.
3650 For example:
3652 REPLACE: .gnome-desktop
3653 WITH: My Desktop
3655 The default setting specifies no patterns.
3656 """
3657 c = event and event.get('c')
3658 if c:
3659 data = c.config.getData('path-demangle')
3660 if data:
3661 g.app.recentFilesManager.demangleRecentFiles(c, data)
3662 else:
3663 g.es_print('No patterns in @data path-demangle')
3664#@+node:ekr.20150514125218.3: *3* enable/disable/toggle-idle-time-events
3665@g.command('disable-idle-time-events')
3666def disable_idle_time_events(event):
3667 """Disable default idle-time event handling."""
3668 g.app.idle_time_hooks_enabled = False
3670@g.command('enable-idle-time-events')
3671def enable_idle_time_events(event):
3672 """Enable default idle-time event handling."""
3673 g.app.idle_time_hooks_enabled = True
3675@g.command('toggle-idle-time-events')
3676def toggle_idle_time_events(event):
3677 """Toggle default idle-time event handling."""
3678 g.app.idle_time_hooks_enabled = not g.app.idle_time_hooks_enabled
3679#@+node:ekr.20150514125218.4: *3* join-leo-irc
3680@g.command('join-leo-irc')
3681def join_leo_irc(event=None):
3682 """Open the web page to Leo's irc channel on freenode.net."""
3683 import webbrowser
3684 webbrowser.open("http://webchat.freenode.net/?channels=%23leo&uio=d4")
3685#@+node:ekr.20150514125218.5: *3* open-url
3686@g.command('open-url')
3687def openUrl(event=None):
3688 """
3689 Open the url in the headline or body text of the selected node.
3691 Use the headline if it contains a valid url.
3692 Otherwise, look *only* at the first line of the body.
3693 """
3694 c = event.get('c')
3695 if c:
3696 g.openUrl(c.p)
3697#@+node:ekr.20150514125218.6: *3* open-url-under-cursor
3698@g.command('open-url-under-cursor')
3699def openUrlUnderCursor(event=None):
3700 """Open the url under the cursor."""
3701 return g.openUrlOnClick(event)
3702#@-others
3703#@@language python
3704#@@tabwidth -4
3705#@@pagewidth 70
3706#@-leo