a little log viewer app. not really complete yet, just playing around

This commit is contained in:
sawka
2024-11-15 13:55:17 -08:00
parent 03dbeef1af
commit 8833e24277
3 changed files with 406 additions and 0 deletions
+329
View File
@@ -0,0 +1,329 @@
package main
import (
"context"
_ "embed"
"flag"
"fmt"
"os"
"regexp"
"github.com/wavetermdev/waveterm/pkg/util/logview"
"github.com/wavetermdev/waveterm/pkg/vdom"
"github.com/wavetermdev/waveterm/pkg/waveapp"
)
//go:embed style.css
var styleCSS []byte
// CLI arguments
var windowSize = flag.Int64("l", 20, "number of lines to display")
var logFilePath string
var AppClient = waveapp.MakeClient(waveapp.AppOpts{
CloseOnCtrlC: true,
GlobalKeyboardEvents: true,
GlobalStyles: styleCSS,
})
// Props types
type LogContentProps struct {
Lines [][]byte `json:"lines"`
Error string `json:"error"`
LineStart int64 `json:"lineStart"`
}
type FilterInputProps struct {
Value string `json:"value"`
OnChange func(string) `json:"onChange"`
OnError func(string) `json:"onError"`
ClearError func() `json:"clearError"`
}
var FilterInput = waveapp.DefineComponent[FilterInputProps](AppClient, "FilterInput",
func(ctx context.Context, props FilterInputProps) any {
handleChange := func(e vdom.VDomEvent) {
filter := e.TargetValue
props.OnChange(filter)
}
return vdom.H("div", map[string]any{
"className": "filter-container",
},
vdom.H("input", map[string]any{
"type": "text",
"className": "filter-input",
"placeholder": "Filter regexp (e.g. 'error|warn')",
"value": props.Value,
"onChange": handleChange,
}),
)
},
)
// LogContent component to display the log lines
var LogContent = waveapp.DefineComponent[LogContentProps](AppClient, "LogContent",
func(ctx context.Context, props LogContentProps) any {
if props.Error != "" {
return vdom.H("div", map[string]any{
"className": "log-error",
}, props.Error)
}
return vdom.H("pre", map[string]any{
"className": "log-content",
},
vdom.ForEachIdx(props.Lines, func(line []byte, idx int) any {
lineNum := props.LineStart + int64(idx)
return vdom.H("div", map[string]any{
"key": idx,
"className": "log-line",
},
vdom.H("span", map[string]any{
"className": "line-number",
}, fmt.Sprintf("%6d ", lineNum)),
vdom.H("span", map[string]any{
"className": "line-content",
}, string(line)),
)
}),
)
},
)
// Main App component
var App = waveapp.DefineComponent(AppClient, "App",
func(ctx context.Context, _ any) any {
// State for storing log lines and error
lines, setLines := vdom.UseState(ctx, [][]byte{})
errorMsg, setErrorMsg := vdom.UseState(ctx, "")
currentLineNum, setCurrentLineNum := vdom.UseState(ctx, int64(0))
filterText, setFilterText := vdom.UseState(ctx, "")
currentLinePtr := vdom.UseRef(ctx, (*logview.LinePtr)(nil))
logViewRef := vdom.UseRef(ctx, (*logview.LogView)(nil))
// Handle filter changes
handleFilterChange := func(filter string) {
setFilterText(filter)
if logViewRef.Current == nil {
return
}
// If filter is empty, clear the regexp
if filter == "" {
logViewRef.Current.MatchRe = nil
} else {
// Try to compile the regexp
re, err := regexp.Compile(filter)
if err != nil {
setErrorMsg(fmt.Sprintf("Invalid regexp: %v", err))
return
}
setErrorMsg("")
logViewRef.Current.MatchRe = re
}
// Reset to first matching line
newPtr, err := logViewRef.Current.FirstLinePtr()
if err != nil {
setErrorMsg(fmt.Sprintf("Error finding first matching line: %v", err))
return
}
if newPtr == nil {
setLines([][]byte{})
setCurrentLineNum(0)
currentLinePtr.Current = nil
return
}
currentLinePtr.Current = newPtr
setCurrentLineNum(newPtr.LineNum)
// Read new window of lines
newLines, err := logViewRef.Current.ReadWindow(newPtr, int(*windowSize))
if err != nil {
setErrorMsg(fmt.Sprintf("Error reading lines: %v", err))
return
}
setLines(newLines)
}
// Load initial log data and setup keyboard handler
vdom.UseEffect(ctx, func() func() {
file, err := os.Open(logFilePath)
if err != nil {
setErrorMsg(fmt.Sprintf("Error opening file: %v", err))
return nil
}
lv := logview.MakeLogView(file)
logViewRef.Current = lv
// Setup keyboard handler
AppClient.SetGlobalEventHandler(func(client *waveapp.Client, event vdom.VDomEvent) {
if event.EventType != "onKeyDown" || event.KeyData == nil {
return
}
if logViewRef.Current == nil {
return
}
key := event.KeyData.Key
var newPtr *logview.LinePtr
var err error
switch key {
case "Home":
newPtr, err = logViewRef.Current.FirstLinePtr()
if err != nil {
setErrorMsg(fmt.Sprintf("Error moving to first line: %v", err))
return
}
case "End":
lastPtr, err := logViewRef.Current.LastLinePtr(currentLinePtr.Current)
if err != nil {
setErrorMsg(fmt.Sprintf("Error moving to last line: %v", err))
return
}
if lastPtr == nil {
return
}
_, backPtr, err := logViewRef.Current.Move(lastPtr, -int(*windowSize-1))
if err != nil {
setErrorMsg(fmt.Sprintf("Error adjusting view: %v", err))
return
}
if backPtr != nil {
newPtr = backPtr
} else {
newPtr, _ = logViewRef.Current.FirstLinePtr()
}
case "ArrowUp":
if currentLinePtr.Current == nil {
newPtr, _ = logViewRef.Current.FirstLinePtr()
} else {
_, newPtr, _ = logViewRef.Current.Move(currentLinePtr.Current, -1)
}
case "ArrowDown":
if currentLinePtr.Current == nil {
newPtr, _ = logViewRef.Current.FirstLinePtr()
} else {
_, newPtr, _ = logViewRef.Current.Move(currentLinePtr.Current, 1)
}
case "PageUp":
if currentLinePtr.Current == nil {
newPtr, _ = logViewRef.Current.FirstLinePtr()
} else if currentLinePtr.Current.LineNum < *windowSize {
newPtr, _ = logViewRef.Current.FirstLinePtr()
} else {
_, newPtr, _ = logViewRef.Current.Move(currentLinePtr.Current, -int(*windowSize))
}
case "PageDown":
if currentLinePtr.Current == nil {
newPtr, _ = logViewRef.Current.FirstLinePtr()
} else {
_, newPtr, _ = logViewRef.Current.Move(currentLinePtr.Current, int(*windowSize))
}
default:
return
}
if newPtr == nil {
return
}
currentLinePtr.Current = newPtr
// Read new window of lines
newLines, err := logViewRef.Current.ReadWindow(newPtr, int(*windowSize))
if err != nil {
setErrorMsg(fmt.Sprintf("Error reading lines: %v", err))
return
}
setLines(newLines)
setCurrentLineNum(newPtr.LineNum)
client.SendAsyncInitiation()
})
// Get first line pointer
linePtr, err := lv.FirstLinePtr()
if err != nil {
setErrorMsg(fmt.Sprintf("Error getting first line: %v", err))
return nil
}
if linePtr == nil {
setLines([][]byte{})
return nil
}
currentLinePtr.Current = linePtr
setCurrentLineNum(linePtr.LineNum)
// Read initial window of lines
initialLines, err := lv.ReadWindow(linePtr, int(*windowSize))
if err != nil {
setErrorMsg(fmt.Sprintf("Error reading lines: %v", err))
return nil
}
setLines(initialLines)
// Cleanup function
return func() {
lv.Close()
file.Close()
}
}, []any{})
return vdom.H("div", map[string]any{
"className": "log-viewer",
},
vdom.H("div", map[string]any{
"className": "log-header",
},
vdom.H("h1", nil, "Log Viewer"),
vdom.H("div", map[string]any{
"className": "log-info",
},
"Showing ", *windowSize, " lines starting at line ", currentLineNum,
" of ", logFilePath),
FilterInput(FilterInputProps{
Value: filterText,
OnChange: handleFilterChange,
}),
),
LogContent(LogContentProps{
Lines: lines,
Error: errorMsg,
LineStart: currentLineNum,
}),
)
},
)
func main() {
AppClient.RegisterDefaultFlags()
flag.Parse()
if flag.NArg() != 1 {
fmt.Fprintf(os.Stderr, "Usage: logviewer [flags] <logfile>\n")
flag.PrintDefaults()
os.Exit(1)
}
logFilePath = flag.Arg(0)
AppClient.RunMain()
}
+75
View File
@@ -0,0 +1,75 @@
.log-viewer {
padding: 1rem;
font-family: monospace;
height: calc(100vh - 2rem);
display: flex;
flex-direction: column;
}
.log-header {
margin-bottom: 1rem;
flex: 0 0 auto;
}
.log-info {
color: #666;
margin-bottom: 1rem;
}
.filter-container {
margin-bottom: 1rem;
}
.filter-input {
width: 100%;
padding: 0.5rem;
font-family: monospace;
background: rgba(0, 0, 0, 0.2);
border: 1px solid #666;
border-radius: 4px;
color: #fff;
}
.filter-input::placeholder {
color: #666;
}
.filter-input:focus {
outline: none;
border-color: #888;
}
.log-content {
background: rgba(0, 0, 0, 0.2);
padding: 1rem;
border-radius: 4px;
margin: 0;
flex: 1 1 auto;
overflow: auto;
white-space: pre;
}
.log-line {
padding: 0.125rem 0;
}
.log-line:hover {
background: rgba(255, 255, 255, 0.1);
}
.line-number {
color: #666;
padding-right: 1rem;
user-select: none;
}
.line-content {
color: #fff;
}
.log-error {
color: #ff4444;
padding: 1rem;
background: rgba(255, 0, 0, 0.1);
border-radius: 4px;
}
+2
View File
@@ -224,6 +224,8 @@ var AppClient *waveapp.Client = waveapp.MakeClient(waveapp.AppOpts{
})
```
Note that the default environment where these apps will launch is a dark mode terminal. So try to pick text colors and background colors that will work well against a dark client. Normally the background is transparent (to a dark-ish background). So either try to leave the background transparent (untouched), or if the transparency is not desirable, then choose a dark background color that would be appropriate.
## Component Definition Pattern
Create typed, reusable components using the client: