1 from __future__
import division
, absolute_import
, unicode_literals
3 from qtpy
.QtCore
import Qt
4 from qtpy
.QtCore
import Signal
5 from qtpy
import QtCore
7 from qtpy
import QtWidgets
9 from ..models
.browse
import GitRepoModel
10 from ..models
.browse
import GitRepoNameItem
11 from ..models
.selection
import State
13 from ..interaction
import Interaction
14 from ..models
import browse
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,
29 settings
=None, show
=True):
30 """Create a new worktree browser"""
31 view
= Browser(context
, parent
, update
=update
, settings
=settings
)
32 model
= GitRepoModel(context
, view
.tree
)
41 def save_path(context
, path
, model
):
42 """Choose an output filename based on the selected path"""
43 filename
= qtutils
.save_as(path
)
45 model
.filename
= filename
46 cmds
.do(SaveBlob
, context
, model
)
53 class Browser(standard
.Widget
):
56 # Read-only mode property
57 mode
= property(lambda self
: self
.model
.mode
)
59 def __init__(self
, context
, parent
, update
=True, settings
=None):
60 standard
.Widget
.__init
__(self
, parent
)
61 self
.settings
= settings
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
.updated
.connect(self
._updated
_callback
, type=Qt
.QueuedConnection
)
68 self
.model
= context
.model
69 self
.model
.add_observer(self
.model
.message_updated
, self
.model_updated
)
71 qtutils
.add_close_action(self
)
75 self
.init_state(settings
, self
.resize
, 720, 420)
77 def set_model(self
, model
):
79 self
.tree
.set_model(model
)
82 """Refresh the model triggering view updates"""
85 def model_updated(self
):
86 """Update the title with the current branch and directory name."""
89 def _updated_callback(self
):
90 branch
= self
.model
.currentbranch
91 curdir
= core
.getcwd()
92 msg
= N_('Repository: %s') % curdir
94 msg
+= N_('Branch: %s') % branch
97 scope
= dict(project
=self
.model
.project
, branch
=branch
)
98 title
= N_('%(project)s: %(branch)s - Browse') % scope
99 if self
.mode
== self
.model
.mode_amend
:
100 title
+= ' %s' % N_('(Amending)')
101 self
.setWindowTitle(title
)
104 class RepoTreeView(standard
.TreeView
):
105 """Provides a filesystem-like view of a git repository."""
107 about_to_update
= Signal()
110 def __init__(self
, context
, parent
):
111 standard
.TreeView
.__init
__(self
, parent
)
113 self
.context
= context
114 self
.selection
= context
.selection
115 self
.saved_selection
= []
116 self
.saved_current_path
= None
117 self
.saved_open_folders
= set()
118 self
.restoring_selection
= False
120 self
.info_event_type
= browse
.GitRepoInfoEvent
.TYPE
122 self
.setDragEnabled(True)
123 self
.setRootIsDecorated(False)
124 self
.setSortingEnabled(False)
125 self
.setSelectionMode(self
.ExtendedSelection
)
127 # Observe model updates
128 model
= context
.model
129 model
.add_observer(model
.message_about_to_update
,
130 self
.emit_about_to_update
)
131 model
.add_observer(model
.message_updated
, self
.emit_update
)
133 self
.about_to_update
.connect(self
.save_selection
,
134 type=Qt
.QueuedConnection
)
135 self
.updated
.connect(self
.update_actions
, type=Qt
.QueuedConnection
)
137 self
.expanded
.connect(self
.index_expanded
)
139 self
.collapsed
.connect(lambda idx
: self
.size_columns())
140 self
.collapsed
.connect(self
.index_collapsed
)
142 # Sync selection before the key press event changes the model index
143 queued
= Qt
.QueuedConnection
144 self
.index_about_to_change
.connect(self
.sync_selection
, type=queued
)
146 self
.action_history
= qtutils
.add_action_with_status_tip(
147 self
, N_('View History...'),
148 N_('View history for selected paths'),
149 self
.view_history
, hotkeys
.HISTORY
)
151 self
.action_stage
= qtutils
.add_action_with_status_tip(
152 self
, cmds
.StageOrUnstage
.name(),
153 N_('Stage/unstage selected paths for commit'),
154 cmds
.run(cmds
.StageOrUnstage
, context
),
155 hotkeys
.STAGE_SELECTION
)
157 self
.action_untrack
= qtutils
.add_action_with_status_tip(
158 self
, N_('Untrack Selected'), N_('Stop tracking paths'),
159 self
.untrack_selected
)
161 self
.action_rename
= qtutils
.add_action_with_status_tip(
162 self
, N_('Rename'), N_('Rename selected paths'),
163 self
.rename_selected
)
165 self
.action_difftool
= qtutils
.add_action_with_status_tip(
166 self
, cmds
.LaunchDifftool
.name(),
167 N_('Launch git-difftool on the current path'),
168 cmds
.run(cmds
.LaunchDifftool
, context
), hotkeys
.DIFF
)
170 self
.action_difftool_predecessor
= qtutils
.add_action_with_status_tip(
171 self
, N_('Diff Against Predecessor...'),
172 N_('Launch git-difftool against previous versions'),
173 self
.diff_predecessor
, hotkeys
.DIFF_SECONDARY
)
175 self
.action_revert_unstaged
= qtutils
.add_action_with_status_tip(
176 self
, cmds
.RevertUnstagedEdits
.name(),
177 N_('Revert unstaged changes to selected paths'),
178 cmds
.run(cmds
.RevertUnstagedEdits
, context
), hotkeys
.REVERT
)
180 self
.action_revert_uncommitted
= qtutils
.add_action_with_status_tip(
181 self
, cmds
.RevertUncommittedEdits
.name(),
182 N_('Revert uncommitted changes to selected paths'),
183 cmds
.run(cmds
.RevertUncommittedEdits
, context
), hotkeys
.UNDO
)
185 self
.action_editor
= qtutils
.add_action_with_status_tip(
186 self
, cmds
.LaunchEditor
.name(),
187 N_('Edit selected paths'),
188 cmds
.run(cmds
.LaunchEditor
, context
), hotkeys
.EDIT
)
190 self
.action_refresh
= common
.refresh_action(context
, self
)
192 if not utils
.is_win32():
193 self
.action_default_app
= common
.default_app_action(
194 context
, self
, self
.selected_paths
)
196 self
.action_parent_dir
= common
.parent_dir_action(
197 context
, self
, self
.selected_paths
)
199 self
.action_terminal
= common
.terminal_action(
200 context
, self
, self
.selected_paths
)
202 self
.x_width
= QtGui
.QFontMetrics(self
.font()).width('x')
205 def index_expanded(self
, index
):
206 """Update information about a directory as it is expanded."""
207 # Remember open folders so that we can restore them when refreshing
208 item
= self
.name_item_from_index(index
)
209 self
.saved_open_folders
.add(item
.path
)
212 # update information about a directory as it is expanded
219 model
.update_entry(path
)
221 for row
in range(item
.rowCount()):
222 path
= item
.child(row
, 0).path
223 model
.update_entry(path
)
227 def index_collapsed(self
, index
):
228 item
= self
.name_item_from_index(index
)
229 self
.saved_open_folders
.remove(item
.path
)
232 self
.model().refresh()
234 def size_columns(self
):
235 """Set the column widths."""
236 self
.resizeColumnToContents(0)
237 self
.resizeColumnToContents(1)
238 self
.resizeColumnToContents(2)
239 self
.resizeColumnToContents(3)
240 self
.resizeColumnToContents(4)
242 def sizeHintForColumn(self
, column
):
243 x_width
= self
.x_width
258 # Filename and others use the actual content
259 size
= super(RepoTreeView
, self
).sizeHintForColumn(column
)
262 def emit_update(self
):
265 def emit_about_to_update(self
):
266 self
.about_to_update
.emit()
268 def save_selection(self
):
269 selection
= self
.selected_paths()
271 self
.saved_selection
= selection
273 current
= self
.current_item()
275 self
.saved_current_path
= current
.path
278 selection
= self
.selectionModel()
279 flags
= selection
.Select | selection
.Rows
281 self
.restoring_selection
= True
283 # Restore opened folders
285 for path
in sorted(self
.saved_open_folders
):
286 row
= model
.get(path
)
289 index
= row
[0].index()
291 self
.setExpanded(index
, True)
293 # Restore the current item. We do this first, otherwise
294 # setCurrentIndex() can mess with the selection we set below
296 current_path
= self
.saved_current_path
298 row
= model
.get(current_path
)
300 current_index
= row
[0].index()
302 if current_index
and current_index
.isValid():
303 self
.setCurrentIndex(current_index
)
305 # Restore selected items
306 for path
in self
.saved_selection
:
307 row
= model
.get(path
)
310 index
= row
[0].index()
313 selection
.select(index
, flags
)
315 self
.restoring_selection
= False
321 """Respond to GitRepoInfoEvents"""
322 if ev
.type() == self
.info_event_type
:
324 self
.apply_data(ev
.data
)
325 return super(RepoTreeView
, self
).event(ev
)
327 def apply_data(self
, data
):
328 entry
= self
.model().get(data
[0])
330 entry
[1].set_status(data
[1])
331 entry
[2].setText(data
[2])
332 entry
[3].setText(data
[3])
333 entry
[4].setText(data
[4])
335 def update_actions(self
):
336 """Enable/disable actions."""
337 selection
= self
.selected_paths()
338 selected
= bool(selection
)
339 staged
= bool(self
.selected_staged_paths(selection
=selection
))
340 modified
= bool(self
.selected_modified_paths(selection
=selection
))
341 unstaged
= bool(self
.selected_unstaged_paths(selection
=selection
))
342 tracked
= bool(self
.selected_tracked_paths(selection
=selection
))
343 revertable
= staged
or modified
345 self
.action_editor
.setEnabled(selected
)
346 self
.action_history
.setEnabled(selected
)
347 if not utils
.is_win32():
348 self
.action_default_app
.setEnabled(selected
)
349 self
.action_parent_dir
.setEnabled(selected
)
350 self
.action_terminal
.setEnabled(selected
)
352 self
.action_stage
.setEnabled(staged
or unstaged
)
353 self
.action_untrack
.setEnabled(tracked
)
354 self
.action_rename
.setEnabled(tracked
)
355 self
.action_difftool
.setEnabled(staged
or modified
)
356 self
.action_difftool_predecessor
.setEnabled(tracked
)
357 self
.action_revert_unstaged
.setEnabled(revertable
)
358 self
.action_revert_uncommitted
.setEnabled(revertable
)
360 def contextMenuEvent(self
, event
):
361 """Create a context menu."""
362 self
.update_actions()
363 menu
= qtutils
.create_menu(N_('Actions'), self
)
364 menu
.addAction(self
.action_editor
)
365 menu
.addAction(self
.action_stage
)
367 menu
.addAction(self
.action_history
)
368 menu
.addAction(self
.action_difftool
)
369 menu
.addAction(self
.action_difftool_predecessor
)
371 menu
.addAction(self
.action_revert_unstaged
)
372 menu
.addAction(self
.action_revert_uncommitted
)
373 menu
.addAction(self
.action_untrack
)
374 menu
.addAction(self
.action_rename
)
375 if not utils
.is_win32():
377 menu
.addAction(self
.action_default_app
)
378 menu
.addAction(self
.action_parent_dir
)
379 menu
.addAction(self
.action_terminal
)
380 menu
.exec_(self
.mapToGlobal(event
.pos()))
382 def mousePressEvent(self
, event
):
383 """Synchronize the selection on mouse-press."""
384 result
= QtWidgets
.QTreeView
.mousePressEvent(self
, event
)
385 self
.sync_selection()
388 def sync_selection(self
):
389 """Push selection into the selection model."""
394 state
= State(staged
, unmerged
, modified
, untracked
)
396 paths
= self
.selected_paths()
397 model
= self
.context
.model
398 model_staged
= utils
.add_parents(model
.staged
)
399 model_modified
= utils
.add_parents(model
.modified
)
400 model_unmerged
= utils
.add_parents(model
.unmerged
)
401 model_untracked
= utils
.add_parents(model
.untracked
)
404 if path
in model_unmerged
:
405 unmerged
.append(path
)
406 elif path
in model_untracked
:
407 untracked
.append(path
)
408 elif path
in model_staged
:
410 elif path
in model_modified
:
411 modified
.append(path
)
414 # Push the new selection into the model.
415 self
.selection
.set_selection(state
)
418 def selectionChanged(self
, old
, new
):
419 """Override selectionChanged to update available actions."""
420 result
= QtWidgets
.QTreeView
.selectionChanged(self
, old
, new
)
421 if not self
.restoring_selection
:
422 self
.update_actions()
426 def update_diff(self
):
427 context
= self
.context
428 model
= context
.model
429 paths
= self
.sync_selection()
430 if paths
and self
.model().path_is_interesting(paths
[0]):
431 cached
= paths
[0] in model
.staged
432 cmds
.do(cmds
.Diff
, context
, paths
[0], cached
)
434 def set_model(self
, model
):
435 """Set the concrete QAbstractItemModel instance."""
437 model
.restore
.connect(self
.restore
, type=Qt
.QueuedConnection
)
439 def name_item_from_index(self
, model_index
):
440 """Return the name item corresponding to the model index."""
441 index
= model_index
.sibling(model_index
.row(), 0)
442 return self
.model().itemFromIndex(index
)
444 def paths_from_indexes(self
, indexes
):
445 return qtutils
.paths_from_indexes(self
.model(), indexes
,
446 item_type
=GitRepoNameItem
.TYPE
)
448 def selected_paths(self
):
449 """Return the selected paths."""
450 return self
.paths_from_indexes(self
.selectedIndexes())
452 def selected_staged_paths(self
, selection
=None):
453 """Return selected staged paths."""
454 if selection
is None:
455 selection
= self
.selected_paths()
456 model
= self
.context
.model
457 staged
= utils
.add_parents(model
.staged
)
458 return [p
for p
in selection
if p
in staged
]
460 def selected_modified_paths(self
, selection
=None):
461 """Return selected modified paths."""
462 if selection
is None:
463 selection
= self
.selected_paths()
464 model
= self
.context
.model
465 modified
= utils
.add_parents(model
.modified
)
466 return [p
for p
in selection
if p
in modified
]
468 def selected_unstaged_paths(self
, selection
=None):
469 """Return selected unstaged paths."""
470 if selection
is None:
471 selection
= self
.selected_paths()
472 model
= self
.context
.model
473 modified
= utils
.add_parents(model
.modified
)
474 untracked
= utils
.add_parents(model
.untracked
)
475 unstaged
= modified
.union(untracked
)
476 return [p
for p
in selection
if p
in unstaged
]
478 def selected_tracked_paths(self
, selection
=None):
479 """Return selected tracked paths."""
480 if selection
is None:
481 selection
= self
.selected_paths()
482 model
= self
.context
.model
483 staged
= set(self
.selected_staged_paths(selection
=selection
))
484 modified
= set(self
.selected_modified_paths(selection
=selection
))
485 untracked
= utils
.add_parents(model
.untracked
)
486 tracked
= staged
.union(modified
)
487 return [p
for p
in selection
488 if p
not in untracked
or p
in tracked
]
490 def view_history(self
):
491 """Launch the configured history browser path-limited to entries."""
492 paths
= self
.selected_paths()
493 cmds
.do(cmds
.VisualizePaths
, self
.context
, paths
)
495 def untrack_selected(self
):
496 """untrack selected paths."""
497 context
= self
.context
498 cmds
.do(cmds
.Untrack
, context
, self
.selected_tracked_paths())
500 def rename_selected(self
):
501 """untrack selected paths."""
502 context
= self
.context
503 cmds
.do(cmds
.Rename
, context
, self
.selected_tracked_paths())
505 def diff_predecessor(self
):
506 """Diff paths against previous versions."""
507 context
= self
.context
508 paths
= self
.selected_tracked_paths()
509 args
= ['--'] + paths
510 revs
, summaries
= gitcmds
.log_helper(
511 context
, all
=False, extra_args
=args
)
512 commits
= select_commits(
513 context
, N_('Select Previous Version'), revs
, summaries
,
518 cmds
.difftool_launch(context
, left
=commit
, paths
=paths
)
520 def current_path(self
):
521 """Return the path for the current item."""
522 index
= self
.currentIndex()
523 if not index
.isValid():
525 return self
.name_item_from_index(index
).path
528 class BrowseModel(object):
529 """Context data used for browsing branches via git-ls-tree"""
531 def __init__(self
, ref
, filename
=None):
533 self
.relpath
= filename
534 self
.filename
= filename
537 class SaveBlob(cmds
.ContextCommand
):
539 def __init__(self
, context
, model
):
540 super(SaveBlob
, self
).__init
__(context
)
541 self
.browse_model
= model
544 git
= self
.context
.git
545 model
= self
.browse_model
546 ref
= '%s:%s' % (model
.ref
, model
.relpath
)
547 with core
.xopen(model
.filename
, 'wb') as fp
:
548 status
, _
, _
= git
.show(ref
, _stdout
=fp
)
550 msg
= (N_('Saved "%(filename)s" from "%(ref)s" to "%(destination)s"') %
551 dict(filename
=model
.relpath
,
553 destination
=model
.filename
))
554 Interaction
.log_status(status
, msg
, '')
556 Interaction
.information(
558 N_('File saved to "%s"') % model
.filename
)
561 class BrowseBranch(standard
.Dialog
):
564 def browse(cls
, context
, ref
):
565 model
= BrowseModel(ref
)
566 dlg
= cls(context
, model
, parent
=qtutils
.active_window())
567 dlg_model
= GitTreeModel(context
, ref
, dlg
)
568 dlg
.setModel(dlg_model
)
569 dlg
.setWindowTitle(N_('Browsing %s') % model
.ref
)
572 if dlg
.exec_() != dlg
.Accepted
:
576 def __init__(self
, context
, model
, parent
=None):
577 standard
.Dialog
.__init
__(self
, parent
=parent
)
578 if parent
is not None:
579 self
.setWindowModality(Qt
.WindowModal
)
581 # updated for use by commands
582 self
.context
= context
586 self
.tree
= GitTreeWidget(parent
=self
)
587 self
.close_button
= qtutils
.close_button()
590 self
.save
= qtutils
.create_button(text
=text
, enabled
=False,
594 self
.btnlayt
= qtutils
.hbox(defs
.margin
, defs
.spacing
,
595 self
.close_button
, qtutils
.STRETCH
,
598 self
.layt
= qtutils
.vbox(defs
.margin
, defs
.spacing
,
599 self
.tree
, self
.btnlayt
)
600 self
.setLayout(self
.layt
)
603 self
.tree
.path_chosen
.connect(self
.save_path
)
605 self
.tree
.selection_changed
.connect(self
.selection_changed
,
606 type=Qt
.QueuedConnection
)
608 qtutils
.connect_button(self
.close_button
, self
.close
)
609 qtutils
.connect_button(self
.save
, self
.save_blob
)
610 self
.init_size(parent
=parent
)
613 self
.tree
.expandAll()
615 def setModel(self
, model
):
616 self
.tree
.setModel(model
)
618 def path_chosen(self
, path
, close
=True):
619 """Update the model from the view"""
622 model
.filename
= path
626 def save_path(self
, path
):
627 """Choose an output filename based on the selected path"""
628 self
.path_chosen(path
, close
=False)
629 if save_path(self
.context
, path
, self
.model
):
633 """Save the currently selected file"""
634 filenames
= self
.tree
.selected_files()
637 self
.save_path(filenames
[0])
639 def selection_changed(self
):
640 """Update actions based on the current selection"""
641 filenames
= self
.tree
.selected_files()
642 self
.save
.setEnabled(bool(filenames
))
645 class GitTreeWidget(standard
.TreeView
):
647 selection_changed
= Signal()
648 path_chosen
= Signal(object)
650 def __init__(self
, parent
=None):
651 standard
.TreeView
.__init
__(self
, parent
)
652 self
.setHeaderHidden(True)
653 self
.doubleClicked
.connect(self
.double_clicked
)
655 def double_clicked(self
, index
):
656 item
= self
.model().itemFromIndex(index
)
661 self
.path_chosen
.emit(item
.path
)
663 def selected_files(self
):
664 items
= self
.selected_items()
665 return [i
.path
for i
in items
if not i
.is_dir
]
667 def selectionChanged(self
, old_selection
, new_selection
):
668 QtWidgets
.QTreeView
.selectionChanged(self
, old_selection
, new_selection
)
669 self
.selection_changed
.emit()
671 def select_first_file(self
):
672 """Select the first filename in the tree"""
674 idx
= self
.indexAt(QtCore
.QPoint(0, 0))
675 item
= model
.itemFromIndex(idx
)
676 while idx
and idx
.isValid() and item
and item
.is_dir
:
677 idx
= self
.indexBelow(idx
)
678 item
= model
.itemFromIndex(idx
)
680 if idx
and idx
.isValid() and item
:
681 self
.setCurrentIndex(idx
)
684 class GitFileTreeModel(QtGui
.QStandardItemModel
):
685 """Presents a list of file paths as a hierarchical tree."""
687 def __init__(self
, parent
):
688 QtGui
.QStandardItemModel
.__init
__(self
, parent
)
689 self
.dir_entries
= {'': self
.invisibleRootItem()}
693 QtGui
.QStandardItemModel
.clear(self
)
695 self
.dir_entries
= {'': self
.invisibleRootItem()}
697 def add_files(self
, files
):
698 """Add a list of files"""
699 add_file
= self
.add_file
703 def add_file(self
, path
):
704 """Add a file to the model."""
705 dirname
= utils
.dirname(path
)
706 dir_entries
= self
.dir_entries
708 parent
= dir_entries
[dirname
]
710 parent
= dir_entries
[dirname
] = self
.create_dir_entry(dirname
)
712 row_items
= create_row(path
, False)
713 parent
.appendRow(row_items
)
715 def add_directory(self
, parent
, path
):
716 """Add a directory entry to the model."""
718 row_items
= create_row(path
, True)
721 parent_path
= parent
.path
722 except AttributeError: # root QStandardItem
725 # Insert directories before file paths
727 row
= self
.dir_rows
[parent_path
]
729 row
= self
.dir_rows
[parent_path
] = 0
731 parent
.insertRow(row
, row_items
)
732 self
.dir_rows
[parent_path
] += 1
733 self
.dir_entries
[path
] = row_items
[0]
737 def create_dir_entry(self
, dirname
):
739 Create a directory entry for the model.
741 This ensures that directories are always listed before files.
744 entries
= dirname
.split('/')
746 parent
= self
.invisibleRootItem()
747 curdir_append
= curdir
.append
748 self_add_directory
= self
.add_directory
749 dir_entries
= self
.dir_entries
750 for entry
in entries
:
752 path
= '/'.join(curdir
)
754 parent
= dir_entries
[path
]
757 parent
= self_add_directory(grandparent
, path
)
758 dir_entries
[path
] = parent
762 def create_row(path
, is_dir
):
763 """Return a list of items representing a row."""
764 return [GitTreeItem(path
, is_dir
)]
767 class GitTreeModel(GitFileTreeModel
):
769 def __init__(self
, context
, ref
, parent
):
770 GitFileTreeModel
.__init
__(self
, parent
)
771 self
.context
= context
775 def _initialize(self
):
776 """Iterate over git-ls-tree and create GitTreeItems."""
777 git
= self
.context
.git
778 status
, out
, err
= git
.ls_tree(
779 '--full-tree', '-r', '-t', '-z', self
.ref
)
781 Interaction
.log_status(status
, out
, err
)
787 for line
in out
[:-1].split('\0'):
788 # .....6 ...4 ......................................40
789 # 040000 tree c127cde9a0c644a3a8fef449a244f47d5272dfa6 relative
790 # 100644 blob 139e42bf4acaa4927ec9be1ec55a252b97d3f1e2 relative/path
792 relpath
= line
[6 + 1 + 4 + 1 + 40 + 1:]
794 parent
= self
.dir_entries
[utils
.dirname(relpath
)]
795 self
.add_directory(parent
, relpath
)
797 self
.add_file(relpath
)
800 class GitTreeItem(QtGui
.QStandardItem
):
802 Represents a cell in a treeview.
804 Many GitRepoItems could map to a single repository path,
805 but this tree only has a single column.
806 Each GitRepoItem manages a different cell in the tree view.
809 def __init__(self
, path
, is_dir
):
810 QtGui
.QStandardItem
.__init
__(self
)
813 self
.setEditable(False)
814 self
.setDragEnabled(False)
815 self
.setText(utils
.basename(path
))
817 icon
= icons
.directory()
819 icon
= icons
.file_text()