Skip to content

Instantly share code, notes, and snippets.

@scifisatan
Last active June 3, 2025 17:32
Show Gist options
  • Save scifisatan/31736f6dab81010fef4e0cec5ac37a05 to your computer and use it in GitHub Desktop.
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.
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