Skip to content

Commit 5cc5810

Browse files
feat: add native debug viewer in advanced tab (#177)
* feat: add logging package * feat: implement debug viewer and log capture functionality * refactor: streamline log capture and debug viewer functionality * refactor: simplify log display by removing filter options and test log generation * style: adjust margin for log display area in debug viewer * fix: reduce log refresh interval to 100ms for improved responsiveness * style: adjust border radius in OptionContainer and remove unnecessary spacing in AdvancedTab * chore: run `dart format .` * fix: update font to use packaged DM Mono * fix: improve formattedTimestamp method to handle shorter timestamps * fix: conditionally print log messages in debug mode * revert: move docs change to separate commit * fix: refactor DebugViewer layout and wrap in OptionContainer * fix: optimize log refresh logic in DebugViewer for better performance * fix: enhance log handling in LogCapture to prevent duplicates and improve caching * fix: improve log management in LogCapture to handle duplicates and enforce log limits * fix: adjust log formatting in KeyEventService for improved readability * chore: run `dart format .`
1 parent 26650b8 commit 5cc5810

10 files changed

Lines changed: 482 additions & 25 deletions

File tree

lib/app.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,17 @@ class _MainAppState extends ConsumerState<MainApp>
474474
return null;
475475
}
476476

477+
if (call.method == 'receiveLog') {
478+
// Receive logs from other windows for log capture
479+
try {
480+
final logData = Map<String, dynamic>.from(call.arguments as Map);
481+
LogCapture().addReceivedLog(logData);
482+
} catch (_) {
483+
// Ignore malformed log data
484+
}
485+
return null;
486+
}
487+
477488
await _methodCallHandler.handleMethodCall(
478489
call,
479490
ref,

lib/main.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'package:hotkey_manager/hotkey_manager.dart';
99
import 'app.dart';
1010
import 'screens/preferences_screen.dart';
1111
import 'utils/window_controller_extension.dart';
12+
import 'utils/logger.dart';
1213

1314
// Window type definitions
1415
enum WindowType {
@@ -26,6 +27,9 @@ enum WindowType {
2627
Future<void> main(List<String> args) async {
2728
WidgetsFlutterBinding.ensureInitialized();
2829

30+
// Initialize log capture early to catch all logs
31+
LogCapture();
32+
2933
// Get the current window controller
3034
final windowController = await WindowController.fromCurrentEngine();
3135

lib/screens/preferences_screen.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import 'package:overkeys/widgets/tabs/hotkeys_tab.dart';
2424
import 'package:overkeys/widgets/tabs/learn_tab.dart';
2525
import 'package:overkeys/widgets/tabs/advanced_tab.dart';
2626
import 'package:overkeys/widgets/tabs/about_tab.dart';
27+
import 'package:overkeys/utils/logger.dart';
2728

2829
class PreferencesScreen extends ConsumerStatefulWidget {
2930
const PreferencesScreen({super.key, required this.windowController});
@@ -112,6 +113,17 @@ class _PreferencesScreenState extends ConsumerState<PreferencesScreen>
112113
if (call.method == 'requestFocus') {
113114
await windowManager.focus();
114115
}
116+
117+
if (call.method == 'receiveLog' && mounted) {
118+
// Receive logs from other windows (like main window)
119+
try {
120+
final logData = Map<String, dynamic>.from(call.arguments as Map);
121+
LogCapture().addReceivedLog(logData);
122+
} catch (_) {
123+
// Ignore malformed log data
124+
}
125+
}
126+
115127
return null;
116128
});
117129
}

lib/services/key_event_service.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ class KeyEventService {
8181
// Display "Space" in logs for better readability instead of blank
8282
final displayKey = key == ' ' ? 'Space' : key;
8383
_log.debug(
84-
'Key: ${displayKey.padRight(10)}\tKeyCode: ${keyCode.toString().padRight(5)}\tPressed: ${isPressed.toString().padRight(5)}\tShift: $isShiftDown');
84+
'Key: ${displayKey.padRight(10)}\tKeyCode: ${keyCode.toString().padRight(5)}\tPressed: ${isPressed.toString().padRight(7)}\tShift: $isShiftDown');
8585

8686
keyboardNotifier.updateKeyPressState(key, isPressed);
8787

lib/utils/logger.dart

Lines changed: 224 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,244 @@
11
import 'package:flutter/foundation.dart';
2+
import 'package:logging/logging.dart';
3+
import 'package:desktop_multi_window/desktop_multi_window.dart';
24

3-
/// Simple print-based logger with timestamps and log levels
4-
class SimplePrintLogger {
5-
final String name;
5+
/// Log entry representing a single log record
6+
class LogEntry {
7+
final DateTime timestamp;
8+
final String loggerName;
9+
final Level level;
10+
final String message;
11+
final Object? error;
12+
final StackTrace? stackTrace;
613

7-
SimplePrintLogger(this.name);
14+
LogEntry({
15+
required this.timestamp,
16+
required this.loggerName,
17+
required this.level,
18+
required this.message,
19+
this.error,
20+
this.stackTrace,
21+
});
822

9-
String _timestamp() => DateTime.now().toString().substring(11, 23);
23+
String get formattedTimestamp {
24+
final str = timestamp.toString();
25+
if (str.length >= 23) {
26+
return str.substring(11, 23);
27+
}
28+
return str.length > 11 ? str.substring(11) : str;
29+
}
1030

11-
void debug(String message) {
12-
if (kDebugMode) {
13-
print('[${_timestamp()}] [$name] $message');
31+
String get levelEmoji {
32+
if (level == Level.INFO) return 'ℹ️ ';
33+
if (level == Level.WARNING) return '🚧 ';
34+
if (level == Level.SEVERE) return '❌ ';
35+
return '';
36+
}
37+
38+
String get formattedMessage {
39+
StringBuffer buffer = StringBuffer();
40+
buffer.write('[$formattedTimestamp] [$loggerName] $levelEmoji$message');
41+
42+
if (error != null) {
43+
buffer.write('\n[$formattedTimestamp] [$loggerName] $levelEmoji');
44+
buffer.write('Error: $error');
1445
}
46+
47+
if (stackTrace != null) {
48+
buffer.write('\n[$formattedTimestamp] [$loggerName] $levelEmoji');
49+
buffer.write('StackTrace: $stackTrace');
50+
}
51+
52+
return buffer.toString();
1553
}
54+
}
1655

17-
void info(String message) {
18-
if (kDebugMode) {
19-
print('[${_timestamp()}] [$name] ℹ️ $message');
56+
/// Singleton class to capture and store logs
57+
class LogCapture {
58+
static final LogCapture _instance = LogCapture._internal();
59+
factory LogCapture() => _instance;
60+
61+
final List<LogEntry> _logs = [];
62+
final List<LogEntry> _receivedLogs =
63+
[]; // For logs received from other windows
64+
static const int _maxLogs =
65+
1000; // Keep last 1000 combined logs across both buffers
66+
67+
// Cache for combined logs
68+
List<LogEntry>? _cachedCombinedLogs;
69+
bool _isCacheValid = false;
70+
71+
LogCapture._internal() {
72+
Logger.root.level = kDebugMode ? Level.ALL : Level.INFO;
73+
Logger.root.onRecord.listen(_handleLogRecord);
74+
}
75+
76+
void _trimLogsToLimit() {
77+
while (_logs.length + _receivedLogs.length > _maxLogs) {
78+
// Remove the oldest entry from either buffer
79+
if (_logs.isEmpty) {
80+
_receivedLogs.removeAt(0);
81+
} else if (_receivedLogs.isEmpty) {
82+
_logs.removeAt(0);
83+
} else {
84+
// Both have entries, remove from the one with the older timestamp
85+
if (_logs.first.timestamp.isBefore(_receivedLogs.first.timestamp)) {
86+
_logs.removeAt(0);
87+
} else {
88+
_receivedLogs.removeAt(0);
89+
}
90+
}
2091
}
2192
}
2293

23-
void warning(String message, {Object? error, StackTrace? stackTrace}) {
94+
void _handleLogRecord(LogRecord record) {
95+
final entry = LogEntry(
96+
timestamp: record.time,
97+
loggerName: record.loggerName,
98+
level: record.level,
99+
message: record.message,
100+
error: record.error,
101+
stackTrace: record.stackTrace,
102+
);
103+
104+
_logs.add(entry);
105+
_trimLogsToLimit();
106+
_isCacheValid = false;
107+
108+
// Print to console
24109
if (kDebugMode) {
25-
print('[${_timestamp()}] [$name] 🚧 $message');
26-
if (error != null) print('[${_timestamp()}] [$name] 🚧 Error: $error');
27-
if (stackTrace != null) {
28-
print('[${_timestamp()}] [$name] 🚧 StackTrace: $stackTrace');
110+
print(entry.formattedMessage);
111+
}
112+
113+
// Broadcast to other windows for cross-isolate log viewing
114+
_broadcastLog(entry);
115+
}
116+
117+
void _broadcastLog(LogEntry entry) async {
118+
try {
119+
final logMap = {
120+
'timestamp': entry.timestamp.toIso8601String(),
121+
'loggerName': entry.loggerName,
122+
'level': entry.level.value,
123+
'message': entry.message,
124+
'error': entry.error?.toString(),
125+
'stackTrace': entry.stackTrace?.toString(),
126+
};
127+
128+
final controllers = await WindowController.getAll();
129+
for (final controller in controllers) {
130+
controller.invokeMethod('receiveLog', logMap).catchError((_) => null);
29131
}
132+
} catch (_) {
133+
// Silently ignore broadcast errors
30134
}
31135
}
32136

33-
void error(String message, {Object? error, StackTrace? stackTrace}) {
34-
if (kDebugMode) {
35-
print('[${_timestamp()}] [$name] ❌ $message');
36-
if (error != null) print('[${_timestamp()}] [$name] ❌ Error: $error');
37-
if (stackTrace != null) {
38-
print('[${_timestamp()}] [$name] ❌ StackTrace: $stackTrace');
137+
void addReceivedLog(Map<String, dynamic> logData) {
138+
try {
139+
// Validate required fields
140+
final timestamp = logData['timestamp'] as String?;
141+
final loggerName = logData['loggerName'] as String?;
142+
final levelValue = logData['level'] as int?;
143+
final message = logData['message'] as String?;
144+
145+
if (timestamp == null ||
146+
loggerName == null ||
147+
levelValue == null ||
148+
message == null) {
149+
return;
39150
}
151+
152+
final parsedTimestamp = DateTime.parse(timestamp);
153+
final level = Level.LEVELS.firstWhere(
154+
(l) => l.value == levelValue,
155+
orElse: () => Level.INFO,
156+
);
157+
158+
// Check if this log already exists in _logs or _receivedLogs to avoid duplicates
159+
// This happens when a window broadcasts to all windows including itself
160+
// or when the same remote log is received multiple times
161+
final isDuplicate = _logs.any((log) =>
162+
log.timestamp == parsedTimestamp &&
163+
log.loggerName == loggerName &&
164+
log.level == level &&
165+
log.message == message) ||
166+
_receivedLogs.any((log) =>
167+
log.timestamp == parsedTimestamp &&
168+
log.loggerName == loggerName &&
169+
log.level == level &&
170+
log.message == message);
171+
172+
if (isDuplicate) {
173+
return; // Skip adding duplicate from broadcast
174+
}
175+
176+
final entry = LogEntry(
177+
timestamp: parsedTimestamp,
178+
loggerName: loggerName,
179+
level: level,
180+
message: message,
181+
error: logData['error'],
182+
stackTrace:
183+
logData['stackTrace'] != null && logData['stackTrace'] != 'null'
184+
? StackTrace.fromString(logData['stackTrace'])
185+
: null,
186+
);
187+
188+
_receivedLogs.add(entry);
189+
_trimLogsToLimit();
190+
_isCacheValid = false;
191+
} catch (_) {
192+
// Silently ignore malformed log data
40193
}
41194
}
195+
196+
List<LogEntry> get logs {
197+
if (_isCacheValid && _cachedCombinedLogs != null) {
198+
return _cachedCombinedLogs!;
199+
}
200+
201+
// Combine and sort logs from both sources
202+
final combined = [..._logs, ..._receivedLogs];
203+
combined.sort((a, b) => a.timestamp.compareTo(b.timestamp));
204+
_cachedCombinedLogs = List.unmodifiable(combined);
205+
_isCacheValid = true;
206+
return _cachedCombinedLogs!;
207+
}
208+
209+
int get logCount => _logs.length + _receivedLogs.length;
210+
211+
void clear() {
212+
_logs.clear();
213+
_receivedLogs.clear();
214+
_isCacheValid = false;
215+
}
216+
}
217+
218+
// Initialize LogCapture immediately when this file is loaded
219+
// This ensures the listener is set up before any loggers are created
220+
// ignore: unused_element
221+
final _logCaptureInitializer = LogCapture();
222+
223+
/// Simple wrapper around standard Dart Logger for backwards compatibility
224+
class SimplePrintLogger {
225+
final Logger _logger;
226+
227+
SimplePrintLogger(String name) : _logger = Logger(name);
228+
229+
void debug(String message) {
230+
_logger.fine(message);
231+
}
232+
233+
void info(String message) {
234+
_logger.info(message);
235+
}
236+
237+
void warning(String message, {Object? error, StackTrace? stackTrace}) {
238+
_logger.warning(message, error, stackTrace);
239+
}
240+
241+
void error(String message, {Object? error, StackTrace? stackTrace}) {
242+
_logger.severe(message, error, stackTrace);
243+
}
42244
}

0 commit comments

Comments
 (0)