Skip to content
Open
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
111 changes: 107 additions & 4 deletions src/toolong/line_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,91 @@
from rich.text import Text

from textual.app import ComposeResult
from textual.containers import ScrollableContainer
from textual.containers import ScrollableContainer, Horizontal

from textual.reactive import reactive
from textual.widget import Widget
from textual.widgets import Label, Static

from toolong.messages import DismissOverlay


class WordWrapButton(Label):
"""Clickable toggle button for word wrap."""

DEFAULT_CSS = """
WordWrapButton {
color: $success;
&:light {
color: $primary;
}
padding: 0 1 0 0;
&:hover {
text-style: bold underline;
}
&.active {
background: $success 25%;
}
}
"""

word_wrap: reactive[bool] = reactive(True)

def render(self) -> str:
check = "✓" if self.word_wrap else " "
return f"[reverse] {check} [/reverse] Wrap"

def watch_word_wrap(self, word_wrap: bool) -> None:
self.set_class(word_wrap, "active")

def on_click(self) -> None:
panel = self.screen.query_one(LinePanel)
panel.word_wrap = not panel.word_wrap


class CloseButton(Label):
"""Clickable button to close the detail panel."""

DEFAULT_CSS = """
CloseButton {
color: $success;
&:light {
color: $primary;
}
padding: 0 1 0 0;
&:hover {
text-style: bold underline;
}
}
"""

def render(self) -> str:
return "[reverse] X [/reverse] Close"

def on_click(self) -> None:
self.post_message(DismissOverlay())


class PanelToolbar(Horizontal):
"""Toolbar at the top of the detail panel."""

DEFAULT_CSS = """
PanelToolbar {
height: 1;
width: 1fr;
dock: top;
background: $surface;
padding: 0 1;
WordWrapButton {
dock: right;
}
}
"""

def compose(self) -> ComposeResult:
yield CloseButton()
yield WordWrapButton()


class LineDisplay(Widget):
DEFAULT_CSS = """
Expand Down Expand Up @@ -60,14 +140,37 @@ class LinePanel(ScrollableContainer):
background: $panel;
overflow-y: auto;
overflow-x: auto;
border: blank transparent;
scrollbar-gutter: stable;
&:focus {
border: heavy $accent;
&.word-wrap {
overflow-x: hidden;
LineDisplay {
width: 1fr;
Label {
width: 1fr;
}
.json {
width: 1fr;
}
.nl {
width: 1fr;
}
}
}
}
"""

word_wrap: reactive[bool] = reactive(True)

def compose(self) -> ComposeResult:
yield PanelToolbar()

def watch_word_wrap(self, word_wrap: bool) -> None:
self.set_class(word_wrap, "word-wrap")
self.query_one(WordWrapButton).word_wrap = word_wrap

def on_mount(self) -> None:
self.set_class(self.word_wrap, "word-wrap")

async def update(self, line: str, text: Text, timestamp: datetime | None) -> None:
with self.app.batch_update():
await self.query(LineDisplay).remove()
Expand Down
45 changes: 25 additions & 20 deletions src/toolong/log_lines.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,20 +94,29 @@ def run(self) -> None:
except Empty:
continue
else:
self.pending.discard(request)
log_file, index, start, end = request
self.queue.task_done()
if self.exit_event.is_set() or log_file is None:
break
log_lines.post_message(
LineRead(
index,
log_file,
start,
end,
log_file.get_line(start, end),
# Drain the queue to get the most recent batch of requests
batch = [request]
try:
while True:
batch.append(self.queue.get_nowait())
except Empty:
pass
# Process all drained requests
for req in batch:
self.pending.discard(req)
log_file, index, start, end = req
self.queue.task_done()
if self.exit_event.is_set() or log_file is None:
return
log_lines.post_message(
LineRead(
index,
log_file,
start,
end,
log_file.get_line(start, end),
)
)
)


class SearchSuggester(Suggester):
Expand Down Expand Up @@ -151,17 +160,13 @@ class LogLines(ScrollView, inherit_bindings=False):
LogLines {
scrollbar-gutter: stable;
overflow: scroll;
border: heavy transparent;
.loglines--filter-highlight {
background: $secondary;
color: auto;
}
.loglines--pointer-highlight {
background: $primary;
}
&:focus {
border: heavy $accent;
}

border-subtitle-color: $success;
border-subtitle-align: center;
Expand Down Expand Up @@ -910,9 +915,9 @@ def watch_tail(self, tail: bool) -> None:
self.set_class(tail, "-tail")
if tail:
self.update_line_count()
self.scroll_to(y=self.max_scroll_y, animate=False)
if tail:
self.pointer_line = None
self.update_virtual_size()
self.scroll_to(y=self.max_scroll_y, animate=False, force=True)
self.pointer_line = None

def update_line_count(self) -> None:
line_count = len(self._line_breaks.get(self.log_file, []))
Expand Down
35 changes: 31 additions & 4 deletions src/toolong/log_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,31 @@ def render(self) -> str:
return f"[reverse]{self.key_display}[/reverse] {self.description}"

async def on_click(self) -> None:
await self.app.check_bindings(self.key)
await self.app.action_simulate_key(self.key)


class QuitButton(Label):
"""Displays a clickable quit button."""

DEFAULT_CSS = """
QuitButton {
color: $success;
&:light {
color: $primary;
}
padding: 0 1 0 0;
&:hover {
text-style: bold underline;
}
}
"""
DEFAULT_CLASSES = "key"

def render(self) -> str:
return "[reverse] X [/reverse] Close"

def on_click(self) -> None:
self.app.exit()


class MetaLabel(Label):
Expand Down Expand Up @@ -188,6 +212,7 @@ def __init__(self) -> None:
def compose(self) -> ComposeResult:
with Horizontal(classes="key-container"):
pass
yield QuitButton()
yield Label("TAIL", classes="tail")
yield MetaLabel("", classes="meta")

Expand All @@ -203,7 +228,7 @@ async def mount_keys(self) -> None:
await key_container.query("*").remove()
bindings = [
binding
for (_, binding) in self.app.namespace_bindings.values()
for (_, binding, *_rest) in self.app.active_bindings.values()
if binding.show
]

Expand Down Expand Up @@ -267,7 +292,9 @@ class LogView(Horizontal):
width: 1fr;
}
LinePanel {
width: 50%;
width: 1fr;
height: 50%;
dock: top;
display: none;
}
}
Expand Down Expand Up @@ -374,7 +401,7 @@ async def update_panel(self) -> None:

@on(PointerMoved)
async def pointer_moved(self, event: PointerMoved):
if event.pointer_line is None:
if event.pointer_line is None and not self.tail:
self.show_panel = False
if self.show_panel:
await self.update_panel()
Expand Down
5 changes: 3 additions & 2 deletions src/toolong/poll_watcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def run(self) -> None:
watched_file.error_callback(error)
self._file_descriptors.pop(fileno, None)
break
if successful_read:
time.sleep(0.01)
else:
if not successful_read:
time.sleep(0.05)
time.sleep(0.05)