awesome-gocui / gocui

Minimalist Go package aimed at creating Console User Interfaces.
BSD 3-Clause "New" or "Revised" License
344 stars 39 forks source link

Maxlines option on views #30

Open A-UNDERSCORE-D opened 5 years ago

A-UNDERSCORE-D commented 5 years ago

Describe the feature you'd like The ability to clear out the internal buffers either up to the point that the data is being displayed again, or to some arbitrary point provided in the constructor.

There was an issue for something like this on the original repo: https://github.com/jroimartin/gocui/issues/103 and an associated PR: https://github.com/jroimartin/gocui/pull/104

Describe alternatives you've considered Ive tried doing this myself with Clear() and an Fprintf:

        _, y := dataView.Size()
        if dataView.LinesHeight() > y*2 {
            lines := getLast(dataView.BufferLines(), y*1)
            dataView.Clear()
            dataView.Title = fmt.Sprintf("cleanup done at %s", time.Now())
            fmt.Fprint(dataView, strings.Join(lines, "\n"))
        }
// ----------------------
func getLast(ls []string, last int) []string {
    out := make([]string, 0, last)
    if len(ls) < last {
        // fast path, we're not trimming anything
        copy(out, ls)
        return out
    }
    // we need to chomp
    copy(out, ls[len(ls)-last:])
    return out
}

But Clear() seems far to slow to make this work seamlessly. Not to mention that this requires significant extra processing and memory usage to convert the lines out of their internal representation and back.

Reflection magic:

            dv := reflect.Indirect(reflect.ValueOf(dataView))

            buffer := dv.FieldByName("lines")
            buffer = reflect.NewAt(buffer.Type(), unsafe.Pointer(buffer.UnsafeAddr())).Elem()
            l := buffer.Len()
            buffer.Set(buffer.Slice(l-y, l))

            tainted := dv.FieldByName("tainted")
            tainted = reflect.NewAt(tainted.Type(), unsafe.Pointer(tainted.UnsafeAddr())).Elem()
            tainted.Set(reflect.ValueOf(true))

            dataView.Title = fmt.Sprintf("cleanup done at %s", time.Now())
        }
    }

I dont think I really need to point out the issues with this approach. It does work, but reflection to this degree is in general a dangerous idea, and is brittle at best.

Additional context The use case here is a long running TUI that is constantly written to, for me specifically that is for my game management IRC bot's TUI. As that has near constant writes from game server logs.

glvr182 commented 5 years ago

I agree that using reflect this much would be unwise, I am not really sure how to go about this after checking the linked issue and PR, linking @jesseduffield @skanehira @mjarkk for feedback

mjarkk commented 5 years ago

Agree!
Your solutions seems way to complected (but necessary to get it working).
From the original issue this seems like something more users have so i think it's a good idea to add support for this.

gethiox commented 2 years ago

This would be really useful, for now it's unusable for logging purpose, especially when application is supposed to run for a longer time. My temporary solution is to use fixed-sized buffer outside of the view, and overwriting the view with my data which effectively prevents internal buffer from growing.

Simplified example:

type logBuffer struct {
    buffer         [][]byte
    size, position int
}

func newLogBuffer(size int) logBuffer {
    return logBuffer{
        buffer:   make([][]byte, size),
        size:     size,
        position: 0,
    }
}

func (b *logBuffer) WriteMessage(message []byte) {
    b.buffer[b.position] = message
    if b.position+1 == b.size {
        b.position = 0
    } else {
        b.position++
    }
}

func (b *logBuffer) ReadLastMessages(n int) [][]byte {
    if n > b.size {
        n = b.size
    }
    var data = make([][]byte, 0)
    for i := n; i > 0; i-- {
        data = append(data, b.buffer[((b.position-i)%b.size+b.size)%b.size])
    }
    return data
}

Note: it requires additional communication when new messages are available in the buffer and filling up all unused horizontal space of view when writing to view to prevent displaying not overwritten cells. I'm using fixed-sized output messages according to horizontal view size, wrapping capability might require additional care.

_, y := view.Size()
for _, msg := range buf.ReadLastMessages(y) {
    view.Write(msg)
    view.Write([]byte{'\n'})
}

However, my solution is relatively more inefficient on CPU than internal view buffer as whole data needs to be processed and converted into [][]cell format every time the view is being updated that way, and for the platform that I'm currently working with it's pretty significant, but good enough.

I wonder how much work it would take to implement something like this for internal buffer.

dankox commented 2 years ago

This is an old issue which I guess got forgotten, so I just want to clarify what is requested.

We would want something like View.MaxLines field, which would specify what is the maximum amount of lines available for a View and if new lines would be added at the end, lines from the beginning would be removed.

However, what should happen when lines would be added to the beginning or middle and the amount would cause them to be removed because of hitting MaxLines?
Should we just ignore this and maybe document it? Or should it behave differently?