1 from __future__
import absolute_import
, division
, print_function
, unicode_literals
4 from qtpy
.QtCore
import Qt
5 from qtpy
.QtCore
import Signal
6 from qtpy
import QtCore
8 from qtpy
import QtWidgets
10 from ..models
.browse
import GitRepoModel
11 from ..models
.browse
import GitRepoNameItem
12 from ..models
.selection
import State
14 from ..interaction
import Interaction
17 from .. import gitcmds
18 from .. import hotkeys
21 from .. import qtutils
22 from .selectcommits
import select_commits
25 from . import standard
28 def worktree_browser(context
, parent
=None, update
=True, show
=True):
29 """Create a new worktree browser"""
30 view
= Browser(context
, parent
, update
=update
)
32 context
.browser_windows
.append(view
)
33 view
.closed
.connect(context
.browser_windows
.remove
)
34 model
= GitRepoModel(context
, view
.tree
)
43 def save_path(context
, path
, model
):
44 """Choose an output filename based on the selected path"""
45 filename
= qtutils
.save_as(path
)
47 model
.filename
= filename
48 cmds
.do(SaveBlob
, context
, model
)
55 class Browser(standard
.Widget
):
56 """A repository branch file browser. Browses files provided by GitRepoModel"""
57 # Read-only mode property
58 mode
= property(lambda self
: self
.model
.mode
)
60 def __init__(self
, context
, parent
, update
=True):
61 standard
.Widget
.__init
__(self
, parent
)
62 self
.tree
= RepoTreeView(context
, self
)
63 self
.mainlayout
= qtutils
.hbox(defs
.no_margin
, defs
.spacing
, self
.tree
)
64 self
.setLayout(self
.mainlayout
)
66 self
.model
= context
.model
67 self
.model
.updated
.connect(self
._updated
_callback
, type=Qt
.QueuedConnection
)
69 qtutils
.add_close_action(self
)
71 self
._updated
_callback
()
73 self
.init_state(context
.settings
, self
.resize
, 720, 420)
75 def set_model(self
, model
):
77 self
.tree
.set_model(model
)
80 """Refresh the model triggering view updates"""
83 def _updated_callback(self
):
84 branch
= self
.model
.currentbranch
85 curdir
= core
.getcwd()
86 msg
= N_('Repository: %s') % curdir
88 msg
+= N_('Branch: %s') % branch
91 scope
= dict(project
=self
.model
.project
, branch
=branch
)
92 title
= N_('%(project)s: %(branch)s - Browse') % scope
93 if self
.mode
== self
.model
.mode_amend
:
94 title
+= ' %s' % N_('(Amending)')
95 self
.setWindowTitle(title
)
98 # pylint: disable=too-many-ancestors
99 class RepoTreeView(standard
.TreeView
):
100 """Provides a filesystem-like view of a git repository."""
102 def __init__(self
, context
, parent
):
103 standard
.TreeView
.__init
__(self
, parent
)
105 self
.context
= context
106 self
.selection
= context
.selection
107 self
.saved_selection
= []
108 self
.saved_current_path
= None
109 self
.saved_open_folders
= set()
110 self
.restoring_selection
= False
111 self
._columns
_sized
= False
113 self
.setDragEnabled(True)
114 self
.setRootIsDecorated(False)
115 self
.setSortingEnabled(False)
116 self
.setSelectionMode(self
.ExtendedSelection
)
118 # Observe model updates
119 model
= context
.model
120 model
.about_to_update
.connect(self
.save_selection
, type=Qt
.QueuedConnection
)
121 model
.updated
.connect(self
.update_actions
, type=Qt
.QueuedConnection
)
122 self
.expanded
.connect(self
.index_expanded
)
124 self
.collapsed
.connect(lambda idx
: self
.size_columns())
125 self
.collapsed
.connect(self
.index_collapsed
)
127 # Sync selection before the key press event changes the model index
128 queued
= Qt
.QueuedConnection
129 self
.index_about_to_change
.connect(self
.sync_selection
, type=queued
)
131 self
.action_history
= qtutils
.add_action_with_status_tip(
133 N_('View History...'),
134 N_('View history for selected paths'),
139 self
.action_stage
= qtutils
.add_action_with_status_tip(
141 cmds
.StageOrUnstage
.name(),
142 N_('Stage/unstage selected paths for commit'),
143 cmds
.run(cmds
.StageOrUnstage
, context
),
144 hotkeys
.STAGE_SELECTION
,
147 self
.action_untrack
= qtutils
.add_action_with_status_tip(
149 N_('Untrack Selected'),
150 N_('Stop tracking paths'),
151 self
.untrack_selected
,
154 self
.action_rename
= qtutils
.add_action_with_status_tip(
155 self
, N_('Rename'), N_('Rename selected paths'), self
.rename_selected
158 self
.action_difftool
= qtutils
.add_action_with_status_tip(
160 cmds
.LaunchDifftool
.name(),
161 N_('Launch git-difftool on the current path'),
162 cmds
.run(cmds
.LaunchDifftool
, context
),
166 self
.action_difftool_predecessor
= qtutils
.add_action_with_status_tip(
168 N_('Diff Against Predecessor...'),
169 N_('Launch git-difftool against previous versions'),
170 self
.diff_predecessor
,
171 hotkeys
.DIFF_SECONDARY
,
174 self
.action_revert_unstaged
= qtutils
.add_action_with_status_tip(
176 cmds
.RevertUnstagedEdits
.name(),
177 N_('Revert unstaged changes to selected paths'),
178 cmds
.run(cmds
.RevertUnstagedEdits
, context
),
182 self
.action_revert_uncommitted
= qtutils
.add_action_with_status_tip(
184 cmds
.RevertUncommittedEdits
.name(),
185 N_('Revert uncommitted changes to selected paths'),
186 cmds
.run(cmds
.RevertUncommittedEdits
, context
),
190 self
.action_editor
= qtutils
.add_action_with_status_tip(
192 cmds
.LaunchEditor
.name(),
193 N_('Edit selected paths'),
194 cmds
.run(cmds
.LaunchEditor
, context
),
198 self
.action_blame
= qtutils
.add_action_with_status_tip(
200 cmds
.BlamePaths
.name(),
201 N_('Blame selected paths'),
202 cmds
.run(cmds
.BlamePaths
, context
),
205 self
.action_refresh
= common
.refresh_action(context
, self
)
207 self
.action_default_app
= common
.default_app_action(
208 context
, self
, self
.selected_paths
211 self
.action_parent_dir
= common
.parent_dir_action(
212 context
, self
, self
.selected_paths
215 self
.action_terminal
= common
.terminal_action(
216 context
, self
, func
=self
.selected_paths
219 self
.x_width
= QtGui
.QFontMetrics(self
.font()).width('x')
220 self
.size_columns(force
=True)
222 def index_expanded(self
, index
):
223 """Update information about a directory as it is expanded."""
224 # Remember open folders so that we can restore them when refreshing
225 item
= self
.name_item_from_index(index
)
226 self
.saved_open_folders
.add(item
.path
)
229 # update information about a directory as it is expanded
236 model
.update_entry(path
)
238 for row
in range(item
.rowCount()):
239 path
= item
.child(row
, 0).path
240 model
.update_entry(path
)
244 def index_collapsed(self
, index
):
245 item
= self
.name_item_from_index(index
)
246 self
.saved_open_folders
.remove(item
.path
)
249 self
.model().refresh()
251 def size_columns(self
, force
=False):
252 """Set the column widths."""
253 cfg
= self
.context
.cfg
254 should_resize
= cfg
.get('cola.resizebrowsercolumns', default
=False)
255 if not force
and not should_resize
:
257 self
.resizeColumnToContents(0)
258 self
.resizeColumnToContents(1)
259 self
.resizeColumnToContents(2)
260 self
.resizeColumnToContents(3)
261 self
.resizeColumnToContents(4)
263 def sizeHintForColumn(self
, column
):
264 x_width
= self
.x_width
279 # Filename and others use the actual content
280 size
= super(RepoTreeView
, self
).sizeHintForColumn(column
)
283 def save_selection(self
):
284 selection
= self
.selected_paths()
286 self
.saved_selection
= selection
288 current
= self
.current_item()
290 self
.saved_current_path
= current
.path
293 selection
= self
.selectionModel()
294 flags
= selection
.Select | selection
.Rows
296 self
.restoring_selection
= True
298 # Restore opened folders
300 for path
in sorted(self
.saved_open_folders
):
301 row
= model
.get(path
)
304 index
= row
[0].index()
306 self
.setExpanded(index
, True)
308 # Restore the current item. We do this first, otherwise
309 # setCurrentIndex() can mess with the selection we set below
311 current_path
= self
.saved_current_path
313 row
= model
.get(current_path
)
315 current_index
= row
[0].index()
317 if current_index
and current_index
.isValid():
318 self
.setCurrentIndex(current_index
)
320 # Restore selected items
321 for path
in self
.saved_selection
:
322 row
= model
.get(path
)
325 index
= row
[0].index()
328 selection
.select(index
, flags
)
330 self
.restoring_selection
= False
332 # Resize the columns once when cola.resizebrowsercolumns is False.
333 # This provides a good initial size since we will not be resizing
334 # the columns during expand/collapse.
335 if not self
._columns
_sized
:
336 self
._columns
_sized
= True
337 self
.size_columns(force
=True)
341 def update_actions(self
):
342 """Enable/disable actions."""
343 selection
= self
.selected_paths()
344 selected
= bool(selection
)
345 staged
= bool(self
.selected_staged_paths(selection
=selection
))
346 modified
= bool(self
.selected_modified_paths(selection
=selection
))
347 unstaged
= bool(self
.selected_unstaged_paths(selection
=selection
))
348 tracked
= bool(self
.selected_tracked_paths(selection
=selection
))
349 revertable
= staged
or modified
351 self
.action_editor
.setEnabled(selected
)
352 self
.action_history
.setEnabled(selected
)
353 self
.action_default_app
.setEnabled(selected
)
354 self
.action_parent_dir
.setEnabled(selected
)
356 if self
.action_terminal
is not None:
357 self
.action_terminal
.setEnabled(selected
)
359 self
.action_stage
.setEnabled(staged
or unstaged
)
360 self
.action_untrack
.setEnabled(tracked
)
361 self
.action_rename
.setEnabled(tracked
)
362 self
.action_difftool
.setEnabled(staged
or modified
)
363 self
.action_difftool_predecessor
.setEnabled(tracked
)
364 self
.action_revert_unstaged
.setEnabled(revertable
)
365 self
.action_revert_uncommitted
.setEnabled(revertable
)
367 def contextMenuEvent(self
, event
):
368 """Create a context menu."""
369 self
.update_actions()
370 menu
= qtutils
.create_menu(N_('Actions'), self
)
371 menu
.addAction(self
.action_editor
)
372 menu
.addAction(self
.action_stage
)
374 menu
.addAction(self
.action_history
)
375 menu
.addAction(self
.action_difftool
)
376 menu
.addAction(self
.action_difftool_predecessor
)
377 menu
.addAction(self
.action_blame
)
379 menu
.addAction(self
.action_revert_unstaged
)
380 menu
.addAction(self
.action_revert_uncommitted
)
381 menu
.addAction(self
.action_untrack
)
382 menu
.addAction(self
.action_rename
)
384 menu
.addAction(self
.action_default_app
)
385 menu
.addAction(self
.action_parent_dir
)
387 if self
.action_terminal
is not None:
388 menu
.addAction(self
.action_terminal
)
389 menu
.exec_(self
.mapToGlobal(event
.pos()))
391 def mousePressEvent(self
, event
):
392 """Synchronize the selection on mouse-press."""
393 result
= QtWidgets
.QTreeView
.mousePressEvent(self
, event
)
394 self
.sync_selection()
397 def sync_selection(self
):
398 """Push selection into the selection model."""
403 state
= State(staged
, unmerged
, modified
, untracked
)
405 paths
= self
.selected_paths()
406 model
= self
.context
.model
407 model_staged
= utils
.add_parents(model
.staged
)
408 model_modified
= utils
.add_parents(model
.modified
)
409 model_unmerged
= utils
.add_parents(model
.unmerged
)
410 model_untracked
= utils
.add_parents(model
.untracked
)
413 if path
in model_unmerged
:
414 unmerged
.append(path
)
415 elif path
in model_untracked
:
416 untracked
.append(path
)
417 elif path
in model_staged
:
419 elif path
in model_modified
:
420 modified
.append(path
)
423 # Push the new selection into the model.
424 self
.selection
.set_selection(state
)
427 def selectionChanged(self
, old
, new
):
428 """Override selectionChanged to update available actions."""
429 result
= QtWidgets
.QTreeView
.selectionChanged(self
, old
, new
)
430 if not self
.restoring_selection
:
431 self
.update_actions()
435 def update_diff(self
):
436 context
= self
.context
437 model
= context
.model
438 paths
= self
.sync_selection()
439 if paths
and self
.model().path_is_interesting(paths
[0]):
440 cached
= paths
[0] in model
.staged
441 cmds
.do(cmds
.Diff
, context
, paths
[0], cached
)
443 def set_model(self
, model
):
444 """Set the concrete QAbstractItemModel instance."""
446 model
.restore
.connect(self
.restore
, type=Qt
.QueuedConnection
)
448 def name_item_from_index(self
, model_index
):
449 """Return the name item corresponding to the model index."""
450 index
= model_index
.sibling(model_index
.row(), 0)
451 return self
.model().itemFromIndex(index
)
453 def paths_from_indexes(self
, indexes
):
454 return qtutils
.paths_from_indexes(
455 self
.model(), indexes
, item_type
=GitRepoNameItem
.TYPE
458 def selected_paths(self
):
459 """Return the selected paths."""
460 return self
.paths_from_indexes(self
.selectedIndexes())
462 def selected_staged_paths(self
, selection
=None):
463 """Return selected staged paths."""
464 if selection
is None:
465 selection
= self
.selected_paths()
466 model
= self
.context
.model
467 staged
= utils
.add_parents(model
.staged
)
468 return [p
for p
in selection
if p
in staged
]
470 def selected_modified_paths(self
, selection
=None):
471 """Return selected modified paths."""
472 if selection
is None:
473 selection
= self
.selected_paths()
474 model
= self
.context
.model
475 modified
= utils
.add_parents(model
.modified
)
476 return [p
for p
in selection
if p
in modified
]
478 def selected_unstaged_paths(self
, selection
=None):
479 """Return selected unstaged paths."""
480 if selection
is None:
481 selection
= self
.selected_paths()
482 model
= self
.context
.model
483 modified
= utils
.add_parents(model
.modified
)
484 untracked
= utils
.add_parents(model
.untracked
)
485 unstaged
= modified
.union(untracked
)
486 return [p
for p
in selection
if p
in unstaged
]
488 def selected_tracked_paths(self
, selection
=None):
489 """Return selected tracked paths."""
490 if selection
is None:
491 selection
= self
.selected_paths()
492 model
= self
.context
.model
493 staged
= set(self
.selected_staged_paths(selection
=selection
))
494 modified
= set(self
.selected_modified_paths(selection
=selection
))
495 untracked
= utils
.add_parents(model
.untracked
)
496 tracked
= staged
.union(modified
)
497 return [p
for p
in selection
if p
not in untracked
or p
in tracked
]
499 def view_history(self
):
500 """Launch the configured history browser path-limited to entries."""
501 paths
= self
.selected_paths()
502 cmds
.do(cmds
.VisualizePaths
, self
.context
, paths
)
504 def untrack_selected(self
):
505 """untrack selected paths."""
506 context
= self
.context
507 cmds
.do(cmds
.Untrack
, context
, self
.selected_tracked_paths())
509 def rename_selected(self
):
510 """untrack selected paths."""
511 context
= self
.context
512 cmds
.do(cmds
.Rename
, context
, self
.selected_tracked_paths())
514 def diff_predecessor(self
):
515 """Diff paths against previous versions."""
516 context
= self
.context
517 paths
= self
.selected_tracked_paths()
518 args
= ['--'] + paths
519 revs
, summaries
= gitcmds
.log_helper(context
, all
=False, extra_args
=args
)
520 commits
= select_commits(
521 context
, N_('Select Previous Version'), revs
, summaries
, multiselect
=False
526 cmds
.difftool_launch(context
, left
=commit
, paths
=paths
)
528 def current_path(self
):
529 """Return the path for the current item."""
530 index
= self
.currentIndex()
531 if not index
.isValid():
533 return self
.name_item_from_index(index
).path
536 class BrowseModel(object):
537 """Context data used for browsing branches via git-ls-tree"""
539 def __init__(self
, ref
, filename
=None):
541 self
.relpath
= filename
542 self
.filename
= filename
545 class SaveBlob(cmds
.ContextCommand
):
546 def __init__(self
, context
, model
):
547 super(SaveBlob
, self
).__init
__(context
)
548 self
.browse_model
= model
551 git
= self
.context
.git
552 model
= self
.browse_model
553 ref
= '%s:%s' % (model
.ref
, model
.relpath
)
554 with core
.xopen(model
.filename
, 'wb') as fp
:
555 status
, output
, err
= git
.show(ref
, _stdout
=fp
)
557 out
= '# git show %s >%s\n%s' % (
559 shlex
.quote(model
.filename
),
562 Interaction
.command(N_('Error Saving File'), 'git show', status
, out
, err
)
566 msg
= N_('Saved "%(filename)s" from "%(ref)s" to "%(destination)s"') % dict(
567 filename
=model
.relpath
, ref
=model
.ref
, destination
=model
.filename
569 Interaction
.log_status(status
, msg
, '')
571 Interaction
.information(
572 N_('File Saved'), N_('File saved to "%s"') % model
.filename
576 class BrowseBranch(standard
.Dialog
):
578 def browse(cls
, context
, ref
):
579 model
= BrowseModel(ref
)
580 dlg
= cls(context
, model
, parent
=qtutils
.active_window())
581 dlg_model
= GitTreeModel(context
, ref
, dlg
)
582 dlg
.setModel(dlg_model
)
583 dlg
.setWindowTitle(N_('Browsing %s') % model
.ref
)
586 if dlg
.exec_() != dlg
.Accepted
:
590 def __init__(self
, context
, model
, parent
=None):
591 standard
.Dialog
.__init
__(self
, parent
=parent
)
592 if parent
is not None:
593 self
.setWindowModality(Qt
.WindowModal
)
595 # updated for use by commands
596 self
.context
= context
600 self
.tree
= GitTreeWidget(parent
=self
)
601 self
.close_button
= qtutils
.close_button()
604 self
.save
= qtutils
.create_button(text
=text
, enabled
=False, default
=True)
607 self
.btnlayt
= qtutils
.hbox(
608 defs
.margin
, defs
.spacing
, self
.close_button
, qtutils
.STRETCH
, self
.save
611 self
.layt
= qtutils
.vbox(defs
.margin
, defs
.spacing
, self
.tree
, self
.btnlayt
)
612 self
.setLayout(self
.layt
)
615 self
.tree
.path_chosen
.connect(self
.save_path
)
617 self
.tree
.selection_changed
.connect(
618 self
.selection_changed
, type=Qt
.QueuedConnection
621 qtutils
.connect_button(self
.close_button
, self
.close
)
622 qtutils
.connect_button(self
.save
, self
.save_blob
)
623 self
.init_size(parent
=parent
)
626 self
.tree
.expandAll()
628 def setModel(self
, model
):
629 self
.tree
.setModel(model
)
631 def path_chosen(self
, path
, close
=True):
632 """Update the model from the view"""
635 model
.filename
= path
639 def save_path(self
, path
):
640 """Choose an output filename based on the selected path"""
641 self
.path_chosen(path
, close
=False)
642 if save_path(self
.context
, path
, self
.model
):
646 """Save the currently selected file"""
647 filenames
= self
.tree
.selected_files()
650 self
.save_path(filenames
[0])
652 def selection_changed(self
):
653 """Update actions based on the current selection"""
654 filenames
= self
.tree
.selected_files()
655 self
.save
.setEnabled(bool(filenames
))
658 # pylint: disable=too-many-ancestors
659 class GitTreeWidget(standard
.TreeView
):
661 selection_changed
= Signal()
662 path_chosen
= Signal(object)
664 def __init__(self
, parent
=None):
665 standard
.TreeView
.__init
__(self
, parent
)
666 self
.setHeaderHidden(True)
667 # pylint: disable=no-member
668 self
.doubleClicked
.connect(self
.double_clicked
)
670 def double_clicked(self
, index
):
671 item
= self
.model().itemFromIndex(index
)
676 self
.path_chosen
.emit(item
.path
)
678 def selected_files(self
):
679 items
= self
.selected_items()
680 return [i
.path
for i
in items
if not i
.is_dir
]
682 def selectionChanged(self
, old_selection
, new_selection
):
683 QtWidgets
.QTreeView
.selectionChanged(self
, old_selection
, new_selection
)
684 self
.selection_changed
.emit()
686 def select_first_file(self
):
687 """Select the first filename in the tree"""
689 idx
= self
.indexAt(QtCore
.QPoint(0, 0))
690 item
= model
.itemFromIndex(idx
)
691 while idx
and idx
.isValid() and item
and item
.is_dir
:
692 idx
= self
.indexBelow(idx
)
693 item
= model
.itemFromIndex(idx
)
695 if idx
and idx
.isValid() and item
:
696 self
.setCurrentIndex(idx
)
699 class GitFileTreeModel(QtGui
.QStandardItemModel
):
700 """Presents a list of file paths as a hierarchical tree."""
702 def __init__(self
, parent
):
703 QtGui
.QStandardItemModel
.__init
__(self
, parent
)
704 self
.dir_entries
= {'': self
.invisibleRootItem()}
708 QtGui
.QStandardItemModel
.clear(self
)
710 self
.dir_entries
= {'': self
.invisibleRootItem()}
712 def add_files(self
, files
):
713 """Add a list of files"""
714 add_file
= self
.add_file
718 def add_file(self
, path
):
719 """Add a file to the model."""
720 dirname
= utils
.dirname(path
)
721 dir_entries
= self
.dir_entries
723 parent
= dir_entries
[dirname
]
725 parent
= dir_entries
[dirname
] = self
.create_dir_entry(dirname
)
727 row_items
= create_row(path
, False)
728 parent
.appendRow(row_items
)
730 def add_directory(self
, parent
, path
):
731 """Add a directory entry to the model."""
733 row_items
= create_row(path
, True)
736 parent_path
= parent
.path
737 except AttributeError: # root QStandardItem
740 # Insert directories before file paths
742 row
= self
.dir_rows
[parent_path
]
744 row
= self
.dir_rows
[parent_path
] = 0
746 parent
.insertRow(row
, row_items
)
747 self
.dir_rows
[parent_path
] += 1
748 self
.dir_entries
[path
] = row_items
[0]
752 def create_dir_entry(self
, dirname
):
754 Create a directory entry for the model.
756 This ensures that directories are always listed before files.
759 entries
= dirname
.split('/')
761 parent
= self
.invisibleRootItem()
762 curdir_append
= curdir
.append
763 self_add_directory
= self
.add_directory
764 dir_entries
= self
.dir_entries
765 for entry
in entries
:
767 path
= '/'.join(curdir
)
769 parent
= dir_entries
[path
]
772 parent
= self_add_directory(grandparent
, path
)
773 dir_entries
[path
] = parent
777 def create_row(path
, is_dir
):
778 """Return a list of items representing a row."""
779 return [GitTreeItem(path
, is_dir
)]
782 class GitTreeModel(GitFileTreeModel
):
783 def __init__(self
, context
, ref
, parent
):
784 GitFileTreeModel
.__init
__(self
, parent
)
785 self
.context
= context
789 def _initialize(self
):
790 """Iterate over git-ls-tree and create GitTreeItems."""
791 git
= self
.context
.git
792 status
, out
, err
= git
.ls_tree('--full-tree', '-r', '-t', '-z', self
.ref
)
794 Interaction
.log_status(status
, out
, err
)
800 for line
in out
[:-1].split('\0'):
801 # .....6 ...4 ......................................40
802 # 040000 tree c127cde9a0c644a3a8fef449a244f47d5272dfa6 relative
803 # 100644 blob 139e42bf4acaa4927ec9be1ec55a252b97d3f1e2 relative/path
805 relpath
= line
[6 + 1 + 4 + 1 + 40 + 1 :]
807 parent
= self
.dir_entries
[utils
.dirname(relpath
)]
808 self
.add_directory(parent
, relpath
)
810 self
.add_file(relpath
)
813 class GitTreeItem(QtGui
.QStandardItem
):
815 Represents a cell in a treeview.
817 Many GitRepoItems could map to a single repository path,
818 but this tree only has a single column.
819 Each GitRepoItem manages a different cell in the tree view.
823 def __init__(self
, path
, is_dir
):
824 QtGui
.QStandardItem
.__init
__(self
)
827 self
.setEditable(False)
828 self
.setDragEnabled(False)
829 self
.setText(utils
.basename(path
))
831 icon
= icons
.directory()
833 icon
= icons
.file_text()