search and/or read your saved and liked bluesky posts
wails
go
svelte
sqlite
desktop
bluesky
1<script lang="ts">
2 import { Clear, GetEntries } from "../../../wailsjs/go/main/LogService";
3 import { EventsOn } from "../../../wailsjs/runtime/runtime";
4 import { onMount } from "svelte";
5
6 type LogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR";
7
8 type LogEntry = { level: LogLevel; message: string; timestamp: string };
9
10 type Props = { visible: boolean };
11
12 let { visible }: Props = $props();
13
14 let logs = $state<LogEntry[]>([]);
15 let scrollLock = $state(false);
16 let logContainer: HTMLDivElement | undefined = $state(undefined);
17 let filterLevel = $state<LogLevel | "ALL">("ALL");
18
19 const levels: LogLevel[] = ["DEBUG", "INFO", "WARN", "ERROR"];
20
21 function getLevelColor(level: LogLevel): string {
22 switch (level) {
23 case "DEBUG":
24 return "text-gray-500";
25 case "INFO":
26 return "text-primary";
27 case "WARN":
28 return "text-yellow-400";
29 case "ERROR":
30 return "text-red-400";
31 }
32 }
33
34 function getLevelBgColor(level: LogLevel | "ALL"): string {
35 switch (level) {
36 case "DEBUG":
37 return "bg-gray-600";
38 case "INFO":
39 return "bg-blue-600";
40 case "WARN":
41 return "bg-yellow-600";
42 case "ERROR":
43 return "bg-red-600";
44 default:
45 return "bg-gray-600";
46 }
47 }
48
49 function formatTimestamp(timestamp: string): string {
50 const date = new Date(timestamp);
51 return date.toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" });
52 }
53
54 function scrollToBottom() {
55 if (logContainer && !scrollLock) {
56 logContainer.scrollTop = logContainer.scrollHeight;
57 }
58 }
59
60 function toggleScrollLock() {
61 scrollLock = !scrollLock;
62 }
63
64 function setFilterLevel(level: LogLevel | "ALL") {
65 filterLevel = level;
66 }
67
68 function clearLogs() {
69 logs = [];
70 void Clear();
71 }
72
73 function filteredLogs() {
74 if (filterLevel === "ALL") {
75 return logs;
76 }
77 return logs.filter((log) => log.level === filterLevel);
78 }
79
80 onMount(() => {
81 GetEntries()
82 .then((entries) => {
83 logs = entries.map((entry) => ({
84 level: entry.level as LogLevel,
85 message: entry.message,
86 timestamp: entry.timestamp,
87 }));
88 setTimeout(scrollToBottom, 0);
89 })
90 .catch((err) => {
91 console.error("Failed to load logs:", err);
92 });
93
94 EventsOn("log:line", (entry: LogEntry) => {
95 logs = [...logs, entry];
96
97 if (logs.length > 1000) {
98 logs = logs.slice(logs.length - 1000);
99 }
100
101 setTimeout(scrollToBottom, 0);
102 });
103
104 EventsOn("log:cleared", () => {
105 logs = [];
106 });
107 });
108
109 $effect(() => {
110 if (!scrollLock) {
111 setTimeout(scrollToBottom, 0);
112 }
113 });
114</script>
115
116{#if visible}
117 <div class="border-outline flex flex-col border-t bg-black">
118 <!-- Header -->
119 <div class="bg-surface border-outline flex items-center justify-between border-b px-4 py-2">
120 <div class="flex items-center gap-2">
121 <span class="text-bright font-mono text-sm">Logs</span>
122 <span class="text-muted font-mono text-xs">({logs.length})</span>
123 </div>
124
125 <div class="flex items-center gap-2">
126 <!-- Level Filter Buttons -->
127 <div class="mr-4 flex items-center gap-1">
128 {#each ["ALL", ...levels] as level}
129 <button
130 onclick={() => setFilterLevel(level as LogLevel | "ALL")}
131 class="rounded px-2 py-1 font-mono text-xs transition-colors {filterLevel === level
132 ? getLevelBgColor(level) + ' text-white'
133 : 'text-muted hover:text-bright bg-black'}">
134 {level}
135 </button>
136 {/each}
137 </div>
138
139 <!-- Scroll Lock Toggle -->
140 <button
141 onclick={toggleScrollLock}
142 class="rounded px-2 py-1 font-mono text-xs transition-colors {scrollLock
143 ? 'bg-yellow-600 text-white'
144 : 'text-muted hover:text-bright bg-black'}"
145 title={scrollLock ? "Scroll locked" : "Auto-scroll enabled"}>
146 {#if scrollLock}
147 <span class="flex items-center">
148 <i class="i-ri-lock-2-line"></i>
149 </span>
150 {:else}
151 <span class="flex items-center">
152 <i class="i-ri-arrow-down-box-line"></i>
153 </span>
154 {/if}
155 </button>
156
157 <!-- Clear Button -->
158 <button
159 onclick={clearLogs}
160 class="text-muted rounded bg-black px-2 py-1 font-mono text-xs transition-colors hover:text-red-400">
161 Clear
162 </button>
163 </div>
164 </div>
165
166 <!-- Log Container -->
167 <div
168 bind:this={logContainer}
169 class="flex-1 space-y-0.5 overflow-y-auto p-2 font-mono text-xs"
170 style="max-height: 200px;">
171 {#each filteredLogs() as log}
172 <div class="flex items-start gap-2 rounded px-1 hover:bg-white/5">
173 <span class="text-muted shrink-0">{formatTimestamp(log.timestamp)}</span>
174 <span class="{getLevelColor(log.level)} w-12 shrink-0">[{log.level}]</span>
175 <span class="text-bright break-all">{log.message}</span>
176 </div>
177 {:else}
178 <div class="text-muted text-center py-4">No logs</div>
179 {/each}
180 </div>
181 </div>
182{/if}