textual icon indicating copy to clipboard operation
textual copied to clipboard

Logging widget

Open DecrepitHuman opened this issue 3 years ago • 3 comments

I'm in need of a widget that can display logs, I've attempted to implement this myself multiple times. However, the widget refuses to update with the new content.

Example: class LogPanel(Widget): logs = Reactive([])

def write(self, message: str) -> None:
    self.logs.append(message)
    self.refresh()

def render(self) -> Panel:
    return Panel("\n".join(self.logs), title="Logs")

The main app can call the write method without anything happening. According to the source refreshing happens when idle, this does not seem to be working!

This isn't difficult, rather simple yet It's not working and I am unable to explain why.

DecrepitHuman avatar May 26 '22 11:05 DecrepitHuman

In my experience, you have to create the widget instance within the context of the app. There's some global magic going on, and if you try to create it elsewhere, it just breaks silently.

erezsh avatar Jun 01 '22 12:06 erezsh

I found an imperfect workaround,

class Logview(Widget):
    value = Reactive('this is log view')
    def __init__(self,*args):
        super().__init__(*args)
        ## need some placeholder to expand size of this widget
        self.rows = [''] * 60 
    def render(self) -> RenderableType:
        value = '\n'.join(reversed(self.rows))
        #return ScrollView(Panel(value)) ## this does not work
        return Panel(value)
    def write(self,s):
        self.rows.append(s)
        ## just hint the widget to call "render"
        self.value = s

In the on_mount of the app, calls

await self.dock(ScrollView(Logview()), edge="top", name="body")

My suggestion to implement this kind of widget:

class Logview(Widget):
    async def on_mount(self) -> None:
        self.body = Panel()
        self.view = ScrollView(self.body)
    def render(self) -> RenderableType:
        return self.view
    def write(self,s):
        ## update Panel()' content
        self.body.addLine(s)
        ## if there is no auto refresh, manual refreshing is fine.
        self.body.refresh() 
        ## or it is also fine to call refresh() by asyncio.get_event_loop().run_until_complete()
        # asyncio.get_event_loop().run_until_complete(self.body.refresh)

        self.view.scroll_to_bottom()

iapyeh avatar Oct 13 '22 02:10 iapyeh

Finally I got a working copy inspired from https://github.com/Textualize/textual/issues/125

class MyApp(App):
    console_outputs = Text("")
    def write(self,value:str)->None:
        self.console_outputs.append(Text.from_ansi(value+"\n"))
    async def on_mount(self) -> None:
        self.myview = ScrollView()
        await self.view.dock(self.myview)

        loop = asyncio.get_running_loop()
        task = loop.create_task(self.action_console())
        loop.call_soon_threadsafe(task)

    async def action_console(self):
        body = self.myview
        while True:
            pre_y = body.y
            await body.update(self.console_outputs)
            body.y = pre_y
            body.animate("y", body.window.virtual_size.height, duration=1, easing="linear")
            await asyncio.sleep(1)

iapyeh avatar Oct 13 '22 04:10 iapyeh

https://github.com/Textualize/textual/wiki/Sorry-we-closed-your-issue

willmcgugan avatar Oct 25 '22 09:10 willmcgugan

Did we solve your problem?

Glad we could help!

github-actions[bot] avatar Oct 25 '22 09:10 github-actions[bot]