Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [8.0.2] - 2026-03-03

### Changed

- Themes are now in alphabetical order in command palette https://github.com/Textualize/textual/pull/6405

### Fixed

- Fixed issues with Directory Tree https://github.com/Textualize/textual/pull/6405

## [8.0.1] - 2026-03-01

### Fixed
Expand Down Expand Up @@ -3360,6 +3370,7 @@ https://textual.textualize.io/blog/2022/11/08/version-040/#version-040
- New handler system for messages that doesn't require inheritance
- Improved traceback handling

[8.0.2]: https://github.com/Textualize/textual/compare/v8.0.1...v8.0.2
[8.0.1]: https://github.com/Textualize/textual/compare/v8.0.0...v8.0.1
[8.0.0]: https://github.com/Textualize/textual/compare/v7.5.0...v8.0.0
[7.5.0]: https://github.com/Textualize/textual/compare/v7.4.0...v7.5.0
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "textual"
version = "8.0.1"
version = "8.0.2"
homepage = "https://github.com/Textualize/textual"
repository = "https://github.com/Textualize/textual"
documentation = "https://textual.textualize.io/"
Expand Down
3 changes: 2 additions & 1 deletion src/textual/theme.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from dataclasses import dataclass, field
from functools import partial
from operator import attrgetter
from typing import Callable

from textual.command import DiscoveryHit, Hit, Hits, Provider
Expand Down Expand Up @@ -475,7 +476,7 @@ def set_app_theme(name: str) -> None:

return [
(theme.name, partial(set_app_theme, theme.name))
for theme in themes.values()
for theme in sorted(themes.values(), key=attrgetter("name"))
if theme.name != "textual-ansi"
]

Expand Down
24 changes: 14 additions & 10 deletions src/textual/widgets/_directory_tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,7 @@ def _safe_is_dir(path: Path) -> bool:
"""
try:
return path.is_dir()
except PermissionError:
except OSError:
# We may or may not have been looking at a directory, but we
# don't have the rights or permissions to even know that. Best
# we can do, short of letting the error blow up, is assume it's
Expand Down Expand Up @@ -505,7 +505,7 @@ def _directory_content(self, location: Path, worker: Worker) -> Iterator[Path]:
if worker.is_cancelled:
break
yield entry
except PermissionError:
except OSError:
pass

@work(thread=True, exit_on_error=False)
Expand All @@ -526,14 +526,15 @@ def _load_directory(self, node: TreeNode[DirEntry]) -> list[Path]:
key=lambda path: (not self._safe_is_dir(path), path.name.lower()),
)

@work()
@work(exclusive=True, group="_loader")
async def _loader(self) -> None:
"""Background loading queue processor."""
worker = get_current_worker()
load_queue = self._load_queue
while not worker.is_cancelled:
# Get the next node that needs loading off the queue. Note that
# this blocks if the queue is empty.
node = await self._load_queue.get()
node = await load_queue.get()
content: list[Path] = []
async with self.lock:
cursor_node = self.cursor_node
Expand All @@ -557,25 +558,28 @@ async def _loader(self) -> None:
if cursor_node is not None:
self.move_cursor(cursor_node, animate=False)
finally:
# Mark this iteration as done.
self._load_queue.task_done()
load_queue.task_done()

async def _on_tree_node_expanded(self, event: Tree.NodeExpanded[DirEntry]) -> None:
event.stop()
dir_entry = event.node.data
if dir_entry is None:
return
if await asyncio.to_thread(self._safe_is_dir, dir_entry.path):
await self._add_to_load_queue(event.node)
if event.node.data is not None:
await self._add_to_load_queue(event.node)
else:
self.post_message(self.FileSelected(event.node, dir_entry.path))
if event.node.data is not None:
self.post_message(self.FileSelected(event.node, dir_entry.path))

async def _on_tree_node_selected(self, event: Tree.NodeSelected[DirEntry]) -> None:
event.stop()
dir_entry = event.node.data
if dir_entry is None:
return
if await asyncio.to_thread(self._safe_is_dir, dir_entry.path):
self.post_message(self.DirectorySelected(event.node, dir_entry.path))
if event.node.data is not None:
self.post_message(self.DirectorySelected(event.node, dir_entry.path))
else:
self.post_message(self.FileSelected(event.node, dir_entry.path))
if event.node.data is not None:
self.post_message(self.FileSelected(event.node, dir_entry.path))
Loading