@@ -38,6 +38,8 @@ use tauri::menu::{CheckMenuItem, Menu, MenuItem, PredefinedMenuItem, SubmenuBuil
3838use tauri_plugin_dialog:: { DialogExt , MessageDialogKind } ;
3939
4040use crate :: { get_app_handle, get_config, get_tray_id, HANDLE_CONDVAR } ;
41+ use std:: io:: { BufRead , BufReader , Read } ;
42+ use tauri_plugin_notification:: NotificationExt ;
4143
4244#[ derive( Debug ) ]
4345pub enum ModuleMessage {
@@ -358,6 +360,13 @@ fn start_module_thread(
358360 custom_args : Option < Vec < String > > ,
359361 tx : Sender < ModuleMessage > ,
360362) {
363+ // Special handling for aw-notify module
364+ if name == "aw-notify" {
365+ info ! ( "Using special aw-notify handler for module: {name}" ) ;
366+ start_notify_module_thread ( name, path, custom_args, tx) ;
367+ return ;
368+ }
369+
361370 thread:: spawn ( move || {
362371 // Start the child process
363372 let port_string = get_config ( ) . port . to_string ( ) ;
@@ -404,6 +413,136 @@ fn start_module_thread(
404413 } ) ;
405414}
406415
416+ fn start_notify_module_thread (
417+ name : String ,
418+ path : PathBuf ,
419+ custom_args : Option < Vec < String > > ,
420+ tx : Sender < ModuleMessage > ,
421+ ) {
422+ thread:: spawn ( move || {
423+ // Start the child process with --output-only flag
424+ let port_string = get_config ( ) . port . to_string ( ) ;
425+ let mut command = Command :: new ( & path) ;
426+
427+ // Always add --output-only flag for aw-notify
428+ let mut args = vec ! [ "--output-only" . to_string( ) ] ;
429+
430+ // Add any custom args
431+ if let Some ( ref custom) = custom_args {
432+ args. extend_from_slice ( custom) ;
433+ }
434+
435+ command. args ( & args) ;
436+
437+ // Set creation flags on Windows to hide console window
438+ #[ cfg( windows) ]
439+ command. creation_flags ( CREATE_NO_WINDOW ) ;
440+
441+ let mut child = match command
442+ . stdout ( std:: process:: Stdio :: piped ( ) )
443+ . stderr ( std:: process:: Stdio :: piped ( ) )
444+ . spawn ( )
445+ {
446+ Ok ( child) => child,
447+ Err ( e) => {
448+ let error_msg = e. to_string ( ) ;
449+ if error_msg. contains ( "No such option: --output-only" ) {
450+ info ! ( "aw-notify module doesn't support --output-only, falling back to default behavior" ) ;
451+ // Fallback to default module handler
452+ start_module_thread ( name, path, custom_args, tx) ;
453+ return ;
454+ } else {
455+ error ! ( "Failed to start module {name}: {e}" ) ;
456+ return ;
457+ }
458+ }
459+ } ;
460+
461+ // Send a message to the manager that the module has started
462+ tx. send ( ModuleMessage :: Started {
463+ name : name. to_string ( ) ,
464+ pid : child. id ( ) ,
465+ args : Some ( args) ,
466+ } )
467+ . unwrap ( ) ;
468+
469+ // Read output continuously and parse notifications
470+ let stdout = child. stdout . take ( ) . expect ( "Failed to get stdout" ) ;
471+ let reader = BufReader :: new ( stdout) ;
472+
473+ let mut in_notification = false ;
474+ let mut notification_content = Vec :: new ( ) ;
475+
476+ for line in reader. lines ( ) {
477+ match line {
478+ Ok ( line_content) => {
479+ // Check for notification boundaries (exactly 50 dashes)
480+ if line_content == "-" . repeat ( 50 ) {
481+ if in_notification {
482+ // End of notification - send it
483+ if !notification_content. is_empty ( ) {
484+ let content = notification_content. join ( "\n " ) ;
485+ send_notification ( & content) ;
486+ notification_content. clear ( ) ;
487+ }
488+ in_notification = false ;
489+ } else {
490+ // Start of notification
491+ in_notification = true ;
492+ }
493+ } else if in_notification && !line_content. trim ( ) . is_empty ( ) {
494+ // Collect notification content
495+ notification_content. push ( line_content. clone ( ) ) ;
496+ }
497+ // Log all output for debugging
498+ debug ! ( "aw-notify output: {}" , line_content) ;
499+ }
500+ Err ( e) => {
501+ error ! ( "Error reading aw-notify output: {}" , e) ;
502+ break ;
503+ }
504+ }
505+ }
506+
507+ // Wait for the child to exit
508+ let output = child. wait_with_output ( ) . expect ( "failed to wait on child" ) ;
509+
510+ // Send the process output to the manager
511+ tx. send ( ModuleMessage :: Stopped {
512+ name : name. to_string ( ) ,
513+ output,
514+ } )
515+ . unwrap ( ) ;
516+ } ) ;
517+ }
518+
519+ fn send_notification ( content : & str ) {
520+ // Get app handle and send notification
521+ if let Ok ( app_handle_guard) = get_app_handle ( ) . lock ( ) {
522+ let app_handle = & * app_handle_guard;
523+ let result = app_handle
524+ . notification ( )
525+ . builder ( )
526+ . title ( "ActivityWatch" )
527+ . body ( content)
528+ . show ( ) ;
529+
530+ match result {
531+ Ok ( _) => {
532+ info ! (
533+ "Sent notification: {}" ,
534+ content. lines( ) . next( ) . unwrap_or( "" )
535+ ) ;
536+ }
537+ Err ( e) => {
538+ error ! ( "Failed to send notification: {}" , e) ;
539+ }
540+ }
541+ } else {
542+ error ! ( "Failed to get app handle lock for notification" ) ;
543+ }
544+ }
545+
407546#[ cfg( unix) ]
408547fn discover_modules ( ) -> BTreeMap < String , PathBuf > {
409548 let excluded = [
@@ -481,6 +620,10 @@ fn discover_modules() -> BTreeMap<String, PathBuf> {
481620 }
482621 }
483622
623+ debug ! (
624+ "Discovered modules: {:?}" ,
625+ found_modules. keys( ) . collect:: <Vec <_>>( )
626+ ) ;
484627 found_modules
485628}
486629
0 commit comments