suplemon-editor 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. suplemon/__init__.py +0 -0
  2. suplemon/__main__.py +3 -0
  3. suplemon/cli.py +65 -0
  4. suplemon/config/defaults.json +153 -0
  5. suplemon/config/keymap.json +63 -0
  6. suplemon/config.py +324 -0
  7. suplemon/cursor.py +135 -0
  8. suplemon/editor.py +568 -0
  9. suplemon/file.py +193 -0
  10. suplemon/help.py +327 -0
  11. suplemon/helpers.py +104 -0
  12. suplemon/hex2xterm.py +23 -0
  13. suplemon/key_mappings.py +239 -0
  14. suplemon/lexer.py +88 -0
  15. suplemon/line.py +51 -0
  16. suplemon/linelight/__init__.py +0 -0
  17. suplemon/linelight/color_map.py +9 -0
  18. suplemon/linelight/css.py +21 -0
  19. suplemon/linelight/diff.py +17 -0
  20. suplemon/linelight/html.py +19 -0
  21. suplemon/linelight/js.py +21 -0
  22. suplemon/linelight/json.py +15 -0
  23. suplemon/linelight/lua.py +10 -0
  24. suplemon/linelight/md.py +19 -0
  25. suplemon/linelight/php.py +27 -0
  26. suplemon/linelight/py.py +27 -0
  27. suplemon/logger.py +90 -0
  28. suplemon/main.py +771 -0
  29. suplemon/module_loader.py +98 -0
  30. suplemon/modules/application_state.py +84 -0
  31. suplemon/modules/autocomplete.py +114 -0
  32. suplemon/modules/autodocstring.py +142 -0
  33. suplemon/modules/battery.py +118 -0
  34. suplemon/modules/bulk_delete.py +75 -0
  35. suplemon/modules/clock.py +22 -0
  36. suplemon/modules/comment.py +59 -0
  37. suplemon/modules/config.py +20 -0
  38. suplemon/modules/date.py +22 -0
  39. suplemon/modules/diff.py +42 -0
  40. suplemon/modules/eval.py +43 -0
  41. suplemon/modules/hostname.py +34 -0
  42. suplemon/modules/input_test.py +23 -0
  43. suplemon/modules/keymap.py +20 -0
  44. suplemon/modules/linter.py +274 -0
  45. suplemon/modules/lower.py +21 -0
  46. suplemon/modules/lstrip.py +20 -0
  47. suplemon/modules/paste.py +49 -0
  48. suplemon/modules/reload.py +16 -0
  49. suplemon/modules/replace_all.py +23 -0
  50. suplemon/modules/reverse.py +22 -0
  51. suplemon/modules/rstrip.py +19 -0
  52. suplemon/modules/save.py +16 -0
  53. suplemon/modules/save_all.py +19 -0
  54. suplemon/modules/sort_lines.py +41 -0
  55. suplemon/modules/strip.py +19 -0
  56. suplemon/modules/system_clipboard.py +121 -0
  57. suplemon/modules/tabstospaces.py +18 -0
  58. suplemon/modules/toggle_whitespace.py +18 -0
  59. suplemon/modules/upper.py +21 -0
  60. suplemon/prompt.py +319 -0
  61. suplemon/suplemon_module.py +210 -0
  62. suplemon/themes/8colors.tmTheme +179 -0
  63. suplemon/themes/monokai.tmTheme +284 -0
  64. suplemon/themes.py +201 -0
  65. suplemon/ui.py +656 -0
  66. suplemon/viewer.py +1004 -0
  67. suplemon_editor-0.3.0.dist-info/METADATA +515 -0
  68. suplemon_editor-0.3.0.dist-info/RECORD +72 -0
  69. suplemon_editor-0.3.0.dist-info/WHEEL +5 -0
  70. suplemon_editor-0.3.0.dist-info/entry_points.txt +3 -0
  71. suplemon_editor-0.3.0.dist-info/licenses/LICENSE +21 -0
  72. suplemon_editor-0.3.0.dist-info/top_level.txt +1 -0
suplemon/main.py ADDED
@@ -0,0 +1,771 @@
1
+ # -*- encoding: utf-8
2
+
3
+ """
4
+ The main class that starts and runs Suplemon.
5
+ """
6
+
7
+
8
+ import os
9
+ import sys
10
+
11
+ from . import ui
12
+ from . import module_loader
13
+ from . import themes
14
+ from . import helpers
15
+
16
+ from .file import File
17
+ from .logger import logger
18
+ from .config import Config
19
+ from .editor import Editor
20
+
21
+ __version__ = "0.3.0"
22
+
23
+
24
+ class App:
25
+ def __init__(self, filenames=None, config_file=None, log_level=None):
26
+ """
27
+ Handle App initialization
28
+
29
+ :param list filenames: Names of files to load initially
30
+ :param str filenames[*]: Path to a file to load
31
+ """
32
+ self.version = __version__
33
+ self.inited = False
34
+ self.running = False
35
+ self.debug = True
36
+ self.log_level = log_level
37
+ self.block_rendering = False
38
+
39
+ # Set default variables
40
+ self.path = os.path.dirname(os.path.realpath(__file__))
41
+ self.files = []
42
+ self.current_file = 0
43
+ self.status_msg = ""
44
+ self.last_input = None
45
+ self.global_buffer = []
46
+ self.event_bindings = {}
47
+
48
+ self.config = None
49
+ self.ui = None
50
+ self.modules = None
51
+ self.themes = None
52
+
53
+ # Maximum amount of inputs to process at once
54
+ self.max_input = 100
55
+
56
+ # Save filenames for later
57
+ self.filenames = filenames
58
+
59
+ # Save config file path for later
60
+ self.config_file = config_file
61
+
62
+ # Define core operations
63
+ self.operations = {
64
+ "help": self.help,
65
+ "save_file": self.save_file,
66
+ "run_command": self.query_command,
67
+ "go_to": self.go_to,
68
+ "open": self.open,
69
+ "close_file": self.close_file,
70
+ "new_file": self.new_file,
71
+ "exit": self.ask_exit,
72
+ "ask_exit": self.ask_exit,
73
+ "prev_file": self.prev_file,
74
+ "next_file": self.next_file,
75
+ "save_file_as": self.save_file_as,
76
+ "reload_file": self.reload_file,
77
+ "toggle_mouse": self.toggle_mouse,
78
+ "toggle_fullscreen": self.toggle_fullscreen,
79
+ }
80
+
81
+ # Bind our logger
82
+ self.logger = logger
83
+ self.logger.debug("Starting Suplemon...")
84
+
85
+ def init(self):
86
+ """Initialize the app."""
87
+ # Load core components
88
+ self.config = Config(self)
89
+ if self.config_file:
90
+ self.config.set_path(self.config_file)
91
+ if not self.config.init():
92
+ # Can't run without config
93
+ return False
94
+ self.config.load()
95
+
96
+ # Configure logger
97
+ self.debug = self.config["app"]["debug"]
98
+ debug_level = self.config["app"]["debug_level"]
99
+ if self.log_level is not None:
100
+ debug_level = self.log_level
101
+ self.logger.debug("Setting debug_level to {0}.".format(debug_level))
102
+ self.logger.setLevel(debug_level)
103
+ [handler.setLevel(debug_level) for handler in self.logger.handlers]
104
+
105
+ # Load user interface
106
+ self.ui = ui.UI(self)
107
+ self.ui.init()
108
+
109
+ # Load extension modules
110
+ self.modules = module_loader.ModuleLoader(self)
111
+ self.modules.load()
112
+
113
+ # Load default module configs
114
+ self.config.load_module_configs()
115
+
116
+ # Load themes
117
+ self.themes = themes.ThemeLoader(self)
118
+
119
+ # Indicate that initialization is complete
120
+ self.inited = True
121
+
122
+ return True
123
+
124
+ def exit(self):
125
+ """Stop the main loop and exit."""
126
+ self.trigger_event_before("app_exit")
127
+ self.running = False
128
+
129
+ def run(self):
130
+ """Run the app via the ui wrapper."""
131
+ self.ui.run(self.run_wrapped)
132
+
133
+ def run_wrapped(self, *args):
134
+ """Actually run the app and start mainloop.
135
+
136
+ This shouldn't be called directly. Instead it's passed to the UI wich
137
+ calls it in a safe curses wrapper.
138
+
139
+ :param *args: Not used. Takes any args the wrapper might pass in.
140
+ """
141
+ # Load ui and files etc
142
+ self.load()
143
+ # Initial render
144
+ self.get_editor().refresh()
145
+ self.ui.refresh()
146
+ # Start mainloop
147
+ self.main_loop()
148
+ self.trigger_event_after("app_exit")
149
+ # Unload ui
150
+ self.ui.unload()
151
+
152
+ def load(self):
153
+ """Load the app.
154
+
155
+ Load the UI, open files in self.filenames and finally trigger
156
+ the 'app_loaded' event.
157
+ """
158
+ self.ui.load()
159
+ if sys.version_info < (3, 8):
160
+ ver = ".".join(map(str, sys.version_info[0:2]))
161
+ self.logger.warning("Running Suplemon with Python {version} "
162
+ "isn't officially supported. Please use "
163
+ "Python 3.8 or higher."
164
+ .format(version=ver))
165
+ self.load_files()
166
+ self.running = True
167
+ self.trigger_event_after("app_loaded")
168
+
169
+ def on_input(self, event):
170
+ # Handle the input or give it to the editor
171
+ if not self.handle_input(event):
172
+ # Pass the input to the editor component
173
+ self.get_editor().handle_input(event)
174
+
175
+ def main_loop(self):
176
+ """Run the terminal IO loop until exit() is called."""
177
+ while self.running:
178
+ # Update ui before refreshing it
179
+ self.ui.update()
180
+ self.block_rendering = True
181
+ got_input = False
182
+
183
+ # Run through max 100 inputs (so the view is updated at least every 100 characters)
184
+ for i in range(self.max_input):
185
+ event = self.ui.get_input(False) # non-blocking
186
+
187
+ if not event:
188
+ break # no more inputs to process at this time
189
+
190
+ got_input = True
191
+ self.on_input(event)
192
+
193
+ if not got_input:
194
+ # Wait for input, since there were none already available
195
+ event = self.ui.get_input(True) # blocking
196
+
197
+ if event:
198
+ got_input = True
199
+ self.on_input(event) # PERF: Up to 30% processing time
200
+
201
+ self.block_rendering = False
202
+
203
+ self.trigger_event_after("mainloop")
204
+ # Rendering happens here
205
+ # TODO: Optimize performance. Can make up 45% of processing time in the loop.
206
+ self.get_editor().refresh()
207
+ self.ui.refresh()
208
+
209
+ def get_status(self):
210
+ """Get the current status message.
211
+
212
+ :return: Current status message.
213
+ :rtype: str
214
+ """
215
+ return self.status_msg
216
+
217
+ def get_file_index(self, file_obj):
218
+ """Return the index of file_obj in the file list.
219
+
220
+ :param file_obj: File instance.
221
+ :return: Index of file_obj.
222
+ :rtype: int
223
+ """
224
+ return self.files.index(file_obj)
225
+
226
+ def get_key_bindings(self):
227
+ """Return the list of key bindings."""
228
+ return self.config.key_bindings
229
+
230
+ def get_event_bindings(self):
231
+ """Return the dict of event bindings."""
232
+ return self.event_bindings
233
+
234
+ def set_key_binding(self, key, operation):
235
+ """Bind a key to an operation.
236
+
237
+ Bind operation to be run when key is pressed.
238
+
239
+ :param key: What key or key combination to bind.
240
+ :param str operation: Which operation to run.
241
+ """
242
+ self.config.keymap.prepend({"keys": [key], "command": operation})
243
+
244
+ def set_event_binding(self, event, when, callback):
245
+ """Bind a callback to be run before or after an event.
246
+
247
+ Bind callback to run before or after event occurs. The when parameter
248
+ should be 'before' or 'after'. If using 'before' the callback can
249
+ inhibit running the event if it returns True
250
+
251
+ :param str event: Event to bind to.
252
+ :param str when: String with 'before' or 'after'.
253
+ :param callback: Callback to bind.
254
+ """
255
+ event_bindings = self.get_event_bindings()
256
+ if when not in event_bindings.keys():
257
+ event_bindings[when] = {}
258
+ if event in event_bindings[when].keys():
259
+ event_bindings[when][event].append(callback)
260
+ else:
261
+ event_bindings[when][event] = [callback]
262
+
263
+ def set_status(self, status):
264
+ """Set app status message.
265
+
266
+ :param str status: Status message to show in status bar.
267
+ """
268
+ self.status_msg = str(status)
269
+
270
+ def unsaved_changes(self):
271
+ """Return True if there are unsaved changes in any file."""
272
+ for f in self.files:
273
+ if f.is_changed():
274
+ return True
275
+ return False
276
+
277
+ def unsaved_files(self):
278
+ """Return the files that have unsaved changes.
279
+
280
+ :return: List of files with unsaved changes.
281
+ :rtype: list
282
+ """
283
+ return [f for f in self.files if f.is_changed()]
284
+
285
+ def reload_config(self):
286
+ """Reload configuration."""
287
+ self.config.reload()
288
+ for f in self.files:
289
+ self.setup_editor(f.editor)
290
+ self.trigger_event_after("config_loaded")
291
+ self.ui.resize()
292
+ self.ui.refresh()
293
+
294
+ def handle_input(self, event):
295
+ """Handle an input event.
296
+
297
+ Runs relevant actions based on the event received.
298
+
299
+ :param event: An event instance.
300
+ :return: Boolean indicating if the event was handled.
301
+ :rtype: boolean
302
+ """
303
+ if not event:
304
+ return False
305
+ self.last_input = event
306
+ if event.type == "key":
307
+ return self.handle_key(event)
308
+ elif event.type == "mouse":
309
+ return self.handle_mouse(event)
310
+ return False
311
+
312
+ def handle_key(self, event):
313
+ """Handle a key input event.
314
+
315
+ :param event: Event instance.
316
+ :return: Boolean indicating if event was handled.
317
+ :rtype: boolean
318
+ """
319
+ key_bindings = self.get_key_bindings()
320
+
321
+ operation = None
322
+ if event.key_name in key_bindings.keys():
323
+ operation = key_bindings[event.key_name]
324
+ elif event.key_code in key_bindings.keys():
325
+ operation = key_bindings[event.key_code]
326
+
327
+ if operation in self.operations.keys():
328
+ self.run_operation(operation)
329
+ return True
330
+ elif operation in self.modules.modules.keys():
331
+ self.run_module(operation)
332
+
333
+ return False
334
+
335
+ def handle_mouse(self, event):
336
+ """Handle a mouse input event.
337
+
338
+ :param event: Event instance.
339
+ :return: Boolean indicating if event was handled.
340
+ :rtype: boolean
341
+ """
342
+ editor = self.get_editor()
343
+ if event.mouse_code == 1: # Left mouse button release
344
+ editor.set_single_cursor(event.mouse_pos)
345
+ elif event.mouse_code == 4096: # Right mouse button release
346
+ editor.add_cursor(event.mouse_pos)
347
+ elif event.mouse_code == 524288: # Wheel up
348
+ editor.jump_up()
349
+ elif event.mouse_code == 134217728: # Wheel down(and unfortunately left button drag)
350
+ editor.jump_down()
351
+ else:
352
+ return False
353
+ return True
354
+
355
+ ###########################################################################
356
+ # User Interactions
357
+ ###########################################################################
358
+
359
+ def help(self):
360
+ """Toggle the help document.
361
+ - If current document is help, close it.
362
+ - Otherwise, if help is open, switch to it.
363
+ - Otherwise, open a new file with help text.
364
+ """
365
+ if self.get_file().is_help:
366
+ self.close_file()
367
+ else:
368
+ idx = next((i for i, f in enumerate(self.files) if f.is_help), -1)
369
+ if idx == -1:
370
+ f = self.default_file()
371
+ from . import help
372
+ f.set_data(help.help_text)
373
+ f.is_help = True
374
+ self.files.append(f)
375
+ idx = self.last_file_index()
376
+ self.switch_to_file(idx)
377
+
378
+ def new_file(self, path=None):
379
+ """Open a new empty file.
380
+
381
+ Open a new file and optionally set it's path.
382
+
383
+ :param str path: Optional. Path for file.
384
+ """
385
+ new_file = self.default_file()
386
+ if path:
387
+ new_file.set_path(path)
388
+ self.files.append(new_file)
389
+ self.current_file = self.last_file_index()
390
+ return new_file
391
+
392
+ def exit_question(self):
393
+ """Build the exit confirmation, naming what would be lost.
394
+
395
+ A bare "Exit?" doesn't say that unsaved work is about to be
396
+ discarded, which is the one thing the user needs to know.
397
+
398
+ :return: Question to put to the user.
399
+ :rtype: str
400
+ """
401
+ unsaved = self.unsaved_files()
402
+ if len(unsaved) == 1:
403
+ return "Discard unsaved changes to '{0}' and exit?".format(unsaved[0].get_name())
404
+ return "Discard unsaved changes to {0} files and exit?".format(len(unsaved))
405
+
406
+ def ask_exit(self):
407
+ """Exit if no unsaved changes, else make sure the user really wants to exit."""
408
+ if self.unsaved_changes():
409
+ yes = self.ui.query_bool(self.exit_question())
410
+ if yes:
411
+ self.exit()
412
+ return True
413
+ return False
414
+ self.exit()
415
+ return True
416
+
417
+ def switch_to_file(self, index):
418
+ """Load a default file if no files specified."""
419
+ self.current_file = index
420
+
421
+ def next_file(self):
422
+ """Switch to next file."""
423
+ if len(self.files) < 2:
424
+ return
425
+ cur = self.current_file
426
+ cur += 1
427
+ if cur > len(self.files)-1:
428
+ cur = 0
429
+ self.switch_to_file(cur)
430
+
431
+ def prev_file(self):
432
+ """Switch to previous file."""
433
+ if len(self.files) < 2:
434
+ return
435
+ cur = self.current_file
436
+ cur -= 1
437
+ if cur < 0:
438
+ cur = len(self.files)-1
439
+ self.switch_to_file(cur)
440
+
441
+ def go_to(self):
442
+ """Go to a line or a file (or a line in a specific file with 'name:lineno')."""
443
+ input_str = self.ui.query("Go to:")
444
+ lineno = None
445
+ fname = None
446
+ if input_str is False:
447
+ return False
448
+ if input_str.find(":") != -1:
449
+ parts = input_str.split(":")
450
+ fname = parts[0]
451
+ lineno = parts[1]
452
+ file_index = self.find_file(fname)
453
+ if file_index != -1:
454
+ self.switch_to_file(file_index)
455
+ try:
456
+ input_str = int(lineno)
457
+ self.get_editor().go_to_pos(input_str)
458
+ except ValueError:
459
+ pass
460
+ else:
461
+ try:
462
+ line_no = int(input_str)
463
+ self.get_editor().go_to_pos(line_no)
464
+ except ValueError:
465
+ file_index = self.find_file(input_str)
466
+ if file_index != -1:
467
+ self.switch_to_file(file_index)
468
+
469
+ def find_file(self, s):
470
+ """Return index of file matching string."""
471
+ # REFACTOR: Move to a helper function or implement in a module
472
+
473
+ # Case insensitive matching
474
+ s = s.lower()
475
+
476
+ # First match files beginning with s
477
+ for i, file in enumerate(self.files):
478
+ if file.name.lower().startswith(s):
479
+ return i
480
+
481
+ # Then match any files that contain s
482
+ for i, file in enumerate(self.files):
483
+ if s in file.name.lower():
484
+ return i
485
+
486
+ return -1
487
+
488
+ def run_command(self, data):
489
+ """Run editor commands."""
490
+ parts = data.split(" ")
491
+ cmd = parts[0].lower()
492
+ if cmd in self.operations.keys():
493
+ return self.run_operation(cmd)
494
+
495
+ args = " ".join(parts[1:])
496
+ self.logger.debug("Looking for command '{0}'".format(cmd))
497
+ if cmd in self.modules.modules.keys():
498
+ self.logger.debug("Trying to run command '{0}'".format(cmd))
499
+ self.get_editor().store_action_state(cmd)
500
+ if not self.run_module(cmd, args):
501
+ return False
502
+ else:
503
+ self.set_status("Command '{0}' not found.".format(cmd))
504
+ return False
505
+ return True
506
+
507
+ def run_module(self, module_name, args=""):
508
+ try:
509
+ self.modules.modules[module_name].run(self, self.get_editor(), args)
510
+ return True
511
+ except Exception:
512
+ # Catch any error when running a module just incase
513
+ self.set_status("Running command failed!")
514
+ self.logger.exception("Running command failed!")
515
+ return False
516
+
517
+ def run_operation(self, operation):
518
+ """Run an app core operation."""
519
+ # Support arbitrary callables. TODO: deprecate
520
+ if hasattr(operation, "__call__"):
521
+ return operation()
522
+
523
+ if operation in self.operations.keys():
524
+ cancel = self.trigger_event_before(operation)
525
+ if not cancel:
526
+ result = self.operations[operation]()
527
+ self.trigger_event_after(operation)
528
+ return result
529
+ elif operation in self.modules.modules.keys():
530
+ cancel = self.trigger_event_before(operation)
531
+ if not cancel:
532
+ result = self.modules.modules[operation].run(self, self.get_editor(), "")
533
+ self.trigger_event_after(operation)
534
+ return result
535
+
536
+ return False
537
+
538
+ def trigger_event(self, event, when):
539
+ """Triggers event and runs registered callbacks."""
540
+ status = False
541
+ bindings = self.get_event_bindings()
542
+ if when not in bindings.keys():
543
+ return False
544
+ if event in bindings[when].keys():
545
+ callbacks = bindings[when][event]
546
+ for cb in callbacks:
547
+ try:
548
+ val = cb(event)
549
+ except Exception:
550
+ # Catch all errors in callbacks just incase
551
+ self.logger.error("Failed running callback: {0}".format(cb), exc_info=True)
552
+ continue
553
+ if val:
554
+ status = True
555
+ return status
556
+
557
+ def trigger_event_before(self, event):
558
+ return self.trigger_event(event, "before")
559
+
560
+ def trigger_event_after(self, event):
561
+ return self.trigger_event(event, "after")
562
+
563
+ def toggle_fullscreen(self):
564
+ """Toggle full screen editor."""
565
+ display = self.config["display"]
566
+ show_indicators = not display["show_top_bar"]
567
+ display["show_top_bar"] = show_indicators
568
+ display["show_bottom_bar"] = show_indicators
569
+ display["show_legend"] = show_indicators
570
+ # Virtual curses windows need to be resized
571
+ self.ui.resize()
572
+
573
+ def toggle_mouse(self):
574
+ """Toggle mouse support."""
575
+ # Invert the boolean
576
+ self.config["editor"]["use_mouse"] = not self.config["editor"]["use_mouse"]
577
+ self.ui.setup_mouse()
578
+ if self.config["editor"]["use_mouse"]:
579
+ self.set_status("Mouse enabled")
580
+ else:
581
+ self.set_status("Mouse disabled")
582
+
583
+ def query_command(self):
584
+ """Run editor commands."""
585
+ modules = self.modules.modules.items()
586
+
587
+ # Get built in operations
588
+ completions = [oper for oper in self.operations.keys()]
589
+ # Add runnable modules
590
+ completions += [name for name, m in modules if m.is_runnable()]
591
+
592
+ data = self.ui.query_autocmp("Command:", completions=sorted(completions))
593
+ if not data:
594
+ return False
595
+ self.run_command(data)
596
+
597
+ ###########################################################################
598
+ # Editor operations
599
+ ###########################################################################
600
+
601
+ def new_editor(self):
602
+ """Create a new editor instance."""
603
+ editor = Editor(self, self.ui.editor_win)
604
+ self.setup_editor(editor)
605
+ return editor
606
+
607
+ def get_editor(self):
608
+ """Return the current editor."""
609
+ return self.files[self.current_file].editor
610
+
611
+ def setup_editor(self, editor):
612
+ """Setup an editor instance with configuration."""
613
+ config = self.config["editor"]
614
+ editor.set_config(config)
615
+ editor.init()
616
+
617
+ ###########################################################################
618
+ # File operations
619
+ ###########################################################################
620
+
621
+ def open(self):
622
+ """Ask for file name and try to open it."""
623
+ input_name = self.ui.query_file("Open file:")
624
+ if not input_name:
625
+ return False
626
+ name_row_col = helpers.get_filename_cursor_pos(input_name)
627
+ name = name_row_col["name"]
628
+ exists = self.file_is_open(name)
629
+ if exists:
630
+ self.switch_to_file(self.files.index(exists))
631
+ return True
632
+
633
+ if not self.open_file(**name_row_col):
634
+ self.set_status("Failed to load '{0}'".format(name))
635
+ return False
636
+ self.switch_to_file(self.last_file_index())
637
+ return True
638
+
639
+ def close_file(self):
640
+ """Close current file if user confirms action."""
641
+ if self.get_file().is_changed():
642
+ if not self.ui.query_bool("Close file?"):
643
+ return False
644
+ self.files.pop(self.current_file)
645
+ if not len(self.files):
646
+ self.new_file()
647
+ return False
648
+ if self.current_file == len(self.files):
649
+ self.current_file -= 1
650
+
651
+ def save_file(self, file=False, overwrite=False):
652
+ """Save current file."""
653
+ f = file or self.get_file()
654
+ # Make sure the file has a name
655
+ if not f.get_name():
656
+ return self.save_file_as(f)
657
+ # Warn if the file has changed on disk
658
+ if not overwrite and f.is_changed_on_disk():
659
+ if not self.ui.query_bool("The file was modified since you opened it, save anyway?"):
660
+ return False
661
+ # Save the file
662
+ if f.save():
663
+ self.set_status("Saved [{0}] '{1}'".format(helpers.curr_time_sec(), f.name))
664
+ if f.path() == self.config.path() or f.path() == self.config.keymap_path():
665
+ self.reload_config()
666
+ return True
667
+ self.set_status("Couldn't write to '{0}'".format(f.name))
668
+ return False
669
+
670
+ def save_file_as(self, file=False):
671
+ """Save current file."""
672
+ f = file or self.get_file()
673
+ name_input = self.ui.query_file("Save as:", f.name)
674
+ if not name_input:
675
+ return False
676
+
677
+ target_dir, name = helpers.parse_path(name_input)
678
+ full_path = os.path.join(target_dir, name)
679
+
680
+ if os.path.exists(full_path):
681
+ if not self.ui.query_bool("A file or directory with that name already exists. Overwrite it?"):
682
+ return False
683
+ if target_dir and not os.path.exists(target_dir):
684
+ if self.ui.query_bool("The path doesn't exist, do you want to create it?"):
685
+ self.logger.debug("Creating missing folders in save path.")
686
+ try:
687
+ os.makedirs(target_dir)
688
+ except OSError:
689
+ self.logger.exception("Couldn't create directories!")
690
+ self.ui.set_status("Couldn't create directories!")
691
+ return False
692
+ else:
693
+ return False
694
+ f.set_path(full_path)
695
+ # We can just overwrite the file since the user already confirmed
696
+ return self.save_file(f, overwrite=True)
697
+
698
+ def reload_file(self):
699
+ """Reload the current file."""
700
+ if self.ui.query_bool("Reload '{0}'?".format(self.get_file().name)):
701
+ if self.get_file().reload():
702
+ return True
703
+ return False
704
+
705
+ def get_files(self):
706
+ """Return list of open files."""
707
+ return self.files
708
+
709
+ def get_file(self):
710
+ """Return the current file."""
711
+ return self.files[self.current_file]
712
+
713
+ def last_file_index(self):
714
+ """Get index of last file."""
715
+ cur = len(self.files)-1
716
+ return cur
717
+
718
+ def current_file_index(self):
719
+ """Get index of current file."""
720
+ return self.current_file
721
+
722
+ def open_file(self, name=None, row=0, col=0):
723
+ """Open a file."""
724
+ file = File(self)
725
+ file.set_path(name)
726
+ file.set_editor(self.new_editor())
727
+ if not file.load():
728
+ return False
729
+ file.get_editor().set_single_cursor((col, row))
730
+ file.get_editor().scroll_to_line(row)
731
+ self.files.append(file)
732
+ return True
733
+
734
+ def load_files(self):
735
+ """Try to load all files specified in arguments."""
736
+ if self.filenames:
737
+ for item in self.filenames:
738
+ name_row_col = helpers.get_filename_cursor_pos(item)
739
+ name = name_row_col["name"]
740
+ if os.path.isdir(name):
741
+ continue
742
+ # Avoid opening duplicate files
743
+ if self.file_is_open(name):
744
+ continue
745
+ if not self.open_file(**name_row_col):
746
+ self.new_file(name)
747
+ # If nothing was loaded
748
+ if not self.files:
749
+ self.load_default()
750
+
751
+ def file_is_open(self, path):
752
+ """Check if file is open. Returns a File object or False."""
753
+ for file in self.files:
754
+ if file.path() == os.path.abspath(path):
755
+ return file
756
+ return False
757
+
758
+ def load_default(self):
759
+ """Load a default file if no files specified."""
760
+ file = self.default_file()
761
+ self.files.append(file)
762
+
763
+ def default_file(self):
764
+ """Create the default file."""
765
+ file = File(self)
766
+ file.set_editor(self.new_editor())
767
+ # Specify contents to avoid appearing as modified
768
+ file.set_data("")
769
+ # Set markdown as the default file type
770
+ file.editor.set_file_extension("md")
771
+ return file