Last active
June 3, 2025 17:32
-
-
Save scifisatan/31736f6dab81010fef4e0cec5ac37a05 to your computer and use it in GitHub Desktop.
This is the implementation i got using AI, definitely need some time to process and learn how each component actually works even though I got the overall gist of it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"bufio" | |
"fmt" | |
"os" | |
"os/exec" | |
"os/signal" | |
"strings" | |
"sync" | |
"syscall" | |
"time" | |
"golang.org/x/term" | |
) | |
type LogcatFilter struct { | |
filter string | |
filterMu sync.RWMutex | |
logBuffer []string | |
bufferMu sync.RWMutex | |
maxBuffer int | |
termWidth int | |
termHeight int | |
} | |
func NewLogcatFilter() *LogcatFilter { | |
width, height, err := term.GetSize(int(os.Stdout.Fd())) | |
if err != nil || width == 0 || height == 0 { | |
width = 80 | |
height = 24 | |
} | |
return &LogcatFilter{ | |
maxBuffer: 10000, | |
logBuffer: make([]string, 0), | |
termWidth: width, | |
termHeight: height, | |
} | |
} | |
func (lf *LogcatFilter) UpdateTermSize() { | |
width, height, err := term.GetSize(int(os.Stdout.Fd())) | |
if err == nil && width > 0 && height > 0 { | |
lf.termWidth = width | |
lf.termHeight = height | |
} | |
} | |
func (lf *LogcatFilter) SetFilter(filter string) { | |
lf.filterMu.Lock() | |
defer lf.filterMu.Unlock() | |
lf.filter = strings.ToLower(strings.TrimSpace(filter)) | |
} | |
func (lf *LogcatFilter) GetFilter() string { | |
lf.filterMu.RLock() | |
defer lf.filterMu.RUnlock() | |
return lf.filter | |
} | |
func (lf *LogcatFilter) AddLogLine(line string) { | |
lf.bufferMu.Lock() | |
defer lf.bufferMu.Unlock() | |
lf.logBuffer = append(lf.logBuffer, line) | |
if len(lf.logBuffer) > lf.maxBuffer { | |
lf.logBuffer = lf.logBuffer[1:] | |
} | |
} | |
func (lf *LogcatFilter) MatchesFilter(line string) bool { | |
filter := lf.GetFilter() | |
if filter == "" { | |
return true | |
} | |
return strings.Contains(strings.ToLower(line), filter) | |
} | |
func (lf *LogcatFilter) GetRecentFilteredLogs(count int) []string { | |
lf.bufferMu.RLock() | |
defer lf.bufferMu.RUnlock() | |
filter := lf.GetFilter() | |
var filtered []string | |
for i := len(lf.logBuffer) - 1; i >= 0 && len(filtered) < count; i-- { | |
line := lf.logBuffer[i] | |
if filter == "" || strings.Contains(strings.ToLower(line), filter) { | |
filtered = append([]string{line}, filtered...) | |
} | |
} | |
return filtered | |
} | |
type Display struct { | |
filter *LogcatFilter | |
inputBuffer string | |
cursorPos int | |
mu sync.Mutex | |
logRegionEnd int // Last row of log region | |
inputRow int // Row for input | |
separatorRow int // Row for separator | |
} | |
func NewDisplay(filter *LogcatFilter) *Display { | |
return &Display{ | |
filter: filter, | |
} | |
} | |
func (d *Display) updateLayout() { | |
d.filter.UpdateTermSize() | |
d.separatorRow = d.filter.termHeight - 1 | |
d.inputRow = d.filter.termHeight | |
d.logRegionEnd = d.filter.termHeight - 2 | |
} | |
func (d *Display) setupTerminal() { | |
// Save current terminal state | |
fmt.Print("\033[?1049h") // Alternative screen buffer | |
fmt.Print("\033[H\033[2J") // Clear screen | |
fmt.Print("\033[?25h") // Show cursor | |
d.updateLayout() | |
d.setupScrollRegion() | |
d.drawStaticUI() | |
} | |
func (d *Display) setupScrollRegion() { | |
// Set scrolling region to exclude the bottom 2 lines (separator + input) | |
fmt.Printf("\033[1;%dr", d.logRegionEnd) | |
} | |
func (d *Display) restoreTerminal() { | |
fmt.Print("\033[?25h") // Show cursor | |
fmt.Print("\033[1;%dr", d.filter.termHeight) // Reset scroll region | |
fmt.Print("\033[?1049l") // Exit alternative screen buffer | |
} | |
func (d *Display) drawStaticUI() { | |
d.mu.Lock() | |
defer d.mu.Unlock() | |
// Draw separator (fixed position) | |
fmt.Printf("\033[%d;1H", d.separatorRow) | |
separator := strings.Repeat("─", d.filter.termWidth) | |
fmt.Print("\033[2m" + separator + "\033[0m") | |
// Draw input line (fixed position) | |
d.redrawInputLine() | |
} | |
func (d *Display) redrawInputLine() { | |
fmt.Printf("\033[%d;1H", d.inputRow) | |
fmt.Print("\033[K") // Clear line | |
prompt := "> " | |
displayText := d.inputBuffer | |
// Handle text that's too long for terminal | |
maxInputWidth := d.filter.termWidth - len(prompt) - 1 | |
startPos := 0 | |
if len(displayText) > maxInputWidth { | |
if d.cursorPos > maxInputWidth-10 { | |
startPos = d.cursorPos - maxInputWidth + 10 | |
if startPos < 0 { | |
startPos = 0 | |
} | |
} | |
if startPos+maxInputWidth > len(displayText) { | |
startPos = len(displayText) - maxInputWidth | |
if startPos < 0 { | |
startPos = 0 | |
} | |
} | |
displayText = displayText[startPos:startPos+maxInputWidth] | |
} | |
fmt.Printf("%s%s", prompt, displayText) | |
// Position cursor | |
cursorCol := len(prompt) + d.cursorPos - startPos + 1 | |
if cursorCol > d.filter.termWidth { | |
cursorCol = d.filter.termWidth | |
} | |
if cursorCol < len(prompt)+1 { | |
cursorCol = len(prompt) + 1 | |
} | |
fmt.Printf("\033[%d;%dH", d.inputRow, cursorCol) | |
} | |
func (d *Display) printLogLine(line string) { | |
d.mu.Lock() | |
defer d.mu.Unlock() | |
// Save cursor position | |
fmt.Print("\033[s") | |
// Move to log region and print (will scroll naturally within the region) | |
fmt.Printf("\033[%d;1H", d.logRegionEnd) | |
fmt.Println(line) | |
// Restore cursor position (back to input area) | |
fmt.Print("\033[u") | |
} | |
func (d *Display) refreshLogDisplay() { | |
d.mu.Lock() | |
defer d.mu.Unlock() | |
// Save cursor position | |
fmt.Print("\033[s") | |
// Clear log region | |
for i := 1; i <= d.logRegionEnd; i++ { | |
fmt.Printf("\033[%d;1H\033[K", i) | |
} | |
// Display recent filtered logs | |
logs := d.filter.GetRecentFilteredLogs(d.logRegionEnd) | |
for i, line := range logs { | |
fmt.Printf("\033[%d;1H%s", i+1, line) | |
} | |
// Restore cursor position | |
fmt.Print("\033[u") | |
} | |
func (d *Display) handleResize() { | |
d.updateLayout() | |
// Reset scroll region | |
d.setupScrollRegion() | |
// Redraw everything | |
fmt.Print("\033[H\033[2J") | |
d.refreshLogDisplay() | |
d.drawStaticUI() | |
} | |
func (d *Display) addChar(ch byte) { | |
if d.cursorPos < len(d.inputBuffer) { | |
d.inputBuffer = d.inputBuffer[:d.cursorPos] + string(ch) + d.inputBuffer[d.cursorPos:] | |
} else { | |
d.inputBuffer += string(ch) | |
} | |
d.cursorPos++ | |
d.filter.SetFilter(d.inputBuffer) | |
d.refreshLogDisplay() | |
d.redrawInputLine() | |
} | |
func (d *Display) deleteChar() { | |
if d.cursorPos > 0 { | |
if d.cursorPos < len(d.inputBuffer) { | |
d.inputBuffer = d.inputBuffer[:d.cursorPos-1] + d.inputBuffer[d.cursorPos:] | |
} else { | |
d.inputBuffer = d.inputBuffer[:len(d.inputBuffer)-1] | |
} | |
d.cursorPos-- | |
d.filter.SetFilter(d.inputBuffer) | |
d.refreshLogDisplay() | |
d.redrawInputLine() | |
} | |
} | |
func (d *Display) moveCursorLeft() { | |
if d.cursorPos > 0 { | |
d.cursorPos-- | |
d.redrawInputLine() | |
} | |
} | |
func (d *Display) moveCursorRight() { | |
if d.cursorPos < len(d.inputBuffer) { | |
d.cursorPos++ | |
d.redrawInputLine() | |
} | |
} | |
func (d *Display) clearInput() { | |
d.inputBuffer = "" | |
d.cursorPos = 0 | |
d.filter.SetFilter("") | |
d.refreshLogDisplay() | |
d.redrawInputLine() | |
} | |
func (d *Display) moveToBeginning() { | |
d.cursorPos = 0 | |
d.redrawInputLine() | |
} | |
func (d *Display) moveToEnd() { | |
d.cursorPos = len(d.inputBuffer) | |
d.redrawInputLine() | |
} | |
func main() { | |
if _, err := exec.LookPath("adb"); err != nil { | |
fmt.Fprintf(os.Stderr, "Error: adb command not found. Please install Android SDK platform tools.\n") | |
os.Exit(1) | |
} | |
filter := NewLogcatFilter() | |
display := NewDisplay(filter) | |
oldState, err := term.MakeRaw(int(os.Stdin.Fd())) | |
if err != nil { | |
fmt.Fprintf(os.Stderr, "Error setting up terminal: %v\n", err) | |
os.Exit(1) | |
} | |
display.setupTerminal() | |
cleanup := func() { | |
term.Restore(int(os.Stdin.Fd()), oldState) | |
display.restoreTerminal() | |
} | |
defer cleanup() | |
// Handle signals | |
sigChan := make(chan os.Signal, 1) | |
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM, syscall.SIGWINCH) | |
// Start logcat | |
cmd := exec.Command("adb", "logcat", "-v", "brief") | |
stdout, err := cmd.StdoutPipe() | |
if err != nil { | |
cleanup() | |
fmt.Fprintf(os.Stderr, "Error creating stdout pipe: %v\n", err) | |
os.Exit(1) | |
} | |
if err := cmd.Start(); err != nil { | |
cleanup() | |
fmt.Fprintf(os.Stderr, "Error starting adb logcat: %v\n", err) | |
os.Exit(1) | |
} | |
logChan := make(chan string, 100) | |
inputChan := make(chan []byte, 10) | |
// Read logcat output | |
go func() { | |
scanner := bufio.NewScanner(stdout) | |
headerSkipped := false | |
for scanner.Scan() { | |
line := strings.TrimSpace(scanner.Text()) | |
if line == "" { | |
continue | |
} | |
if !headerSkipped { | |
if strings.Contains(line, "beginning of") || | |
strings.Contains(line, "-----") || | |
len(line) < 10 { | |
continue | |
} | |
headerSkipped = true | |
} | |
logChan <- line | |
} | |
}() | |
// Read user input | |
go func() { | |
buffer := make([]byte, 3) | |
for { | |
n, err := os.Stdin.Read(buffer) | |
if err != nil || n == 0 { | |
time.Sleep(1 * time.Millisecond) | |
continue | |
} | |
inputChan <- buffer[:n] | |
} | |
}() | |
// Main loop | |
for { | |
select { | |
case sig := <-sigChan: | |
switch sig { | |
case syscall.SIGWINCH: | |
display.handleResize() | |
default: | |
cmd.Process.Kill() | |
cleanup() | |
fmt.Println("Exiting...") | |
os.Exit(0) | |
} | |
case logLine := <-logChan: | |
filter.AddLogLine(logLine) | |
if filter.MatchesFilter(logLine) { | |
display.printLogLine(logLine) | |
} | |
case input := <-inputChan: | |
if len(input) == 1 { | |
switch input[0] { | |
case 3: // Ctrl+C | |
cmd.Process.Kill() | |
cleanup() | |
fmt.Println("Exiting...") | |
os.Exit(0) | |
case 127, 8: // Backspace | |
display.deleteChar() | |
case 21: // Ctrl+U | |
display.clearInput() | |
case 1: // Ctrl+A | |
display.moveToBeginning() | |
case 5: // Ctrl+E | |
display.moveToEnd() | |
default: | |
if input[0] >= 32 && input[0] <= 126 { | |
display.addChar(input[0]) | |
} | |
} | |
} else if len(input) == 3 && input[0] == 27 && input[1] == 91 { | |
switch input[2] { | |
case 68: // Left arrow | |
display.moveCursorLeft() | |
case 67: // Right arrow | |
display.moveCursorRight() | |
} | |
} | |
default: | |
time.Sleep(1 * time.Millisecond) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment