discourse-systray 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,517 +5,1015 @@ require "timeout"
5
5
  require "fileutils"
6
6
  require "json"
7
7
 
8
- class DiscourseSystemTray
9
- CONFIG_DIR = File.expand_path("~/.config/discourse-systray")
10
- CONFIG_FILE = File.join(CONFIG_DIR, "config.json")
11
- OPTIONS = { debug: false, path: nil }
12
-
13
- def self.load_or_prompt_config
14
- OptionParser
15
- .new do |opts|
16
- opts.banner = "Usage: systray.rb [options]"
17
- opts.on("--debug", "Enable debug mode") { OPTIONS[:debug] = true }
18
- opts.on("--path PATH", "Set Discourse path") { |p| OPTIONS[:path] = p }
19
- end
20
- .parse!
21
- FileUtils.mkdir_p(CONFIG_DIR) unless Dir.exist?(CONFIG_DIR)
22
-
23
- if OPTIONS[:path]
24
- save_config(path: OPTIONS[:path])
25
- return OPTIONS[:path]
26
- end
27
-
28
- if File.exist?(CONFIG_FILE)
29
- config = JSON.parse(File.read(CONFIG_FILE))
30
- return config["path"] if config["path"] && Dir.exist?(config["path"])
31
- end
8
+ module ::DiscourseSystray
9
+ class Systray
10
+ CONFIG_DIR = File.expand_path("~/.config/discourse-systray")
11
+ CONFIG_FILE = File.join(CONFIG_DIR, "config.json")
12
+ OPTIONS = { debug: false, path: nil }
13
+
14
+ def self.load_or_prompt_config
15
+ OptionParser
16
+ .new do |opts|
17
+ opts.banner = "Usage: systray.rb [options]"
18
+ opts.on("--debug", "Enable debug mode") { OPTIONS[:debug] = true }
19
+ opts.on("--path PATH", "Set Discourse path") do |p|
20
+ OPTIONS[:path] = p
21
+ end
22
+ opts.on("--console", "Enable console mode") do
23
+ OPTIONS[:console] = true
24
+ end
25
+ opts.on("--attach", "Attach to existing systray") do
26
+ OPTIONS[:attach] = true
27
+ end
28
+ end
29
+ .parse!
30
+ FileUtils.mkdir_p(CONFIG_DIR) unless Dir.exist?(CONFIG_DIR)
32
31
 
33
- # Show dialog to get path
34
- dialog =
35
- Gtk::FileChooserDialog.new(
36
- title: "Select Discourse Directory",
37
- parent: nil,
38
- action: :select_folder,
39
- buttons: [["Cancel", :cancel], ["Select", :accept]]
40
- )
32
+ if OPTIONS[:path]
33
+ save_config(path: OPTIONS[:path])
34
+ return OPTIONS[:path]
35
+ end
41
36
 
42
- path = nil
43
- if dialog.run == :accept
44
- path = dialog.filename
45
- save_config(path: path)
46
- else
47
- puts "No Discourse path specified. Exiting."
48
- exit 1
49
- end
37
+ if File.exist?(CONFIG_FILE)
38
+ config = JSON.parse(File.read(CONFIG_FILE))
39
+ return config["path"] if config["path"] && Dir.exist?(config["path"])
40
+ end
50
41
 
51
- dialog.destroy
52
- path
53
- end
42
+ # Show dialog to get path
43
+ dialog =
44
+ Gtk::FileChooserDialog.new(
45
+ title: "Select Discourse Directory",
46
+ parent: nil,
47
+ action: :select_folder,
48
+ buttons: [["Cancel", :cancel], ["Select", :accept]]
49
+ )
50
+
51
+ path = nil
52
+ if dialog.run == :accept
53
+ path = dialog.filename
54
+ save_config(path: path)
55
+ else
56
+ puts "No Discourse path specified. Exiting."
57
+ exit 1
58
+ end
54
59
 
55
- def self.save_config(path:, window_geometry: nil)
56
- config = { path: path }
57
- config[:window_geometry] = window_geometry if window_geometry
58
- File.write(CONFIG_FILE, JSON.generate(config))
59
- nil # Prevent return value from being printed
60
- end
60
+ dialog.destroy
61
+ path
62
+ end
61
63
 
62
- def self.load_config
63
- return {} unless File.exist?(CONFIG_FILE)
64
- JSON.parse(File.read(CONFIG_FILE))
65
- rescue JSON::ParserError
66
- {}
67
- end
68
- BUFFER_SIZE = 2000
69
-
70
- def initialize
71
- @discourse_path = self.class.load_or_prompt_config
72
- @indicator = Gtk::StatusIcon.new
73
- @indicator.pixbuf =
74
- GdkPixbuf::Pixbuf.new(
75
- file: File.join(File.dirname(__FILE__), "../../assets/discourse.png")
76
- )
77
- @indicator.tooltip_text = "Discourse Manager"
78
- @running = false
79
- @ember_output = []
80
- @unicorn_output = []
81
- @processes = {}
82
- @ember_running = false
83
- @unicorn_running = false
84
- @status_window = nil
64
+ def self.save_config(path:, window_geometry: nil)
65
+ config = { path: path }
66
+ config[:window_geometry] = window_geometry if window_geometry
67
+ File.write(CONFIG_FILE, JSON.generate(config))
68
+ nil # Prevent return value from being printed
69
+ end
85
70
 
86
- # Create right-click menu
87
- @indicator.signal_connect("popup-menu") do |tray, button, time|
88
- menu = Gtk::Menu.new
71
+ def self.load_config
72
+ return {} unless File.exist?(CONFIG_FILE)
73
+ JSON.parse(File.read(CONFIG_FILE))
74
+ rescue JSON::ParserError
75
+ {}
76
+ end
77
+ BUFFER_SIZE = 1000
78
+ BUFFER_TRIM_INTERVAL = 30 # seconds
79
+
80
+ def initialize
81
+ puts "DEBUG: Initializing DiscourseSystray" if OPTIONS[:debug]
82
+
83
+ @discourse_path = self.class.load_or_prompt_config unless OPTIONS[:attach]
84
+ puts "DEBUG: Discourse path: #{@discourse_path}" if OPTIONS[:debug]
85
+
86
+ @running = false
87
+ @ember_output = []
88
+ @unicorn_output = []
89
+ @processes = {}
90
+ @ember_running = false
91
+ @unicorn_running = false
92
+ @status_window = nil
93
+ @buffer_trim_timer = nil
94
+
95
+ # Initialize pipe queue for background processing
96
+ initialize_pipe_queue
97
+
98
+ # Add initial welcome message to buffers with timestamp
99
+ timestamp = Time.now.strftime("%H:%M:%S")
100
+ @ember_output << "#{timestamp} - Discourse Ember CLI Log\n"
101
+ @ember_output << "Start Discourse to see Ember CLI logs here.\n"
102
+ @ember_output << "\n"
103
+
104
+ @unicorn_output << "#{timestamp} - Discourse Unicorn Log\n"
105
+ @unicorn_output << "Start Discourse to see Unicorn logs here.\n"
106
+ @unicorn_output << "\n"
107
+
108
+ # Add a visual separator
109
+ @ember_output << "=" * 50 + "\n"
110
+ @unicorn_output << "=" * 50 + "\n"
111
+
112
+ puts "DEBUG: Added initial data to buffers" if OPTIONS[:debug]
113
+ puts "DEBUG: ember_output size: #{@ember_output.size}" if OPTIONS[:debug]
114
+ puts "DEBUG: unicorn_output size: #{@unicorn_output.size}" if OPTIONS[:debug]
115
+
116
+ # Set up periodic buffer trimming
117
+ setup_buffer_trim_timer unless OPTIONS[:attach]
118
+
119
+ puts "DEBUG: Initialized DiscourseSystray with path: #{@discourse_path}" if OPTIONS[:debug]
120
+ end
121
+
122
+ def setup_buffer_trim_timer
123
+ @buffer_trim_timer = GLib::Timeout.add_seconds(BUFFER_TRIM_INTERVAL) do
124
+ trim_buffers
125
+ true # Keep the timer running
126
+ end
127
+ end
128
+
129
+ def trim_buffers
130
+ # Trim buffers if they exceed the buffer size
131
+ if @ember_output.size > BUFFER_SIZE
132
+ excess = @ember_output.size - BUFFER_SIZE
133
+ @ember_output.shift(excess)
134
+ @ember_line_count = [@ember_line_count - excess, 0].max
135
+ end
136
+
137
+ if @unicorn_output.size > BUFFER_SIZE
138
+ excess = @unicorn_output.size - BUFFER_SIZE
139
+ @unicorn_output.shift(excess)
140
+ @unicorn_line_count = [@unicorn_line_count - excess, 0].max
141
+ end
142
+
143
+ true
144
+ end
89
145
 
90
- # Create menu items with icons
91
- start_item = Gtk::ImageMenuItem.new(label: "Start Discourse")
92
- start_item.image =
93
- Gtk::Image.new(icon_name: "media-playback-start", size: :menu)
146
+ def init_systray
147
+ @indicator = Gtk::StatusIcon.new
148
+ @indicator.pixbuf =
149
+ GdkPixbuf::Pixbuf.new(
150
+ file: File.join(File.dirname(__FILE__), "../../assets/discourse.png")
151
+ )
152
+ @indicator.tooltip_text = "Discourse Manager"
153
+
154
+ @indicator.signal_connect("popup-menu") do |tray, button, time|
155
+ menu = Gtk::Menu.new
156
+
157
+ # Create menu items with icons
158
+ start_item = Gtk::ImageMenuItem.new(label: "Start Discourse")
159
+ start_item.image =
160
+ Gtk::Image.new(icon_name: "media-playback-start", size: :menu)
161
+
162
+ stop_item = Gtk::ImageMenuItem.new(label: "Stop Discourse")
163
+ stop_item.image =
164
+ Gtk::Image.new(icon_name: "media-playback-stop", size: :menu)
165
+
166
+ status_item = Gtk::ImageMenuItem.new(label: "Show Status")
167
+ status_item.image =
168
+ Gtk::Image.new(icon_name: "utilities-system-monitor", size: :menu)
169
+
170
+ quit_item = Gtk::ImageMenuItem.new(label: "Quit")
171
+ quit_item.image =
172
+ Gtk::Image.new(icon_name: "application-exit", size: :menu)
173
+
174
+ # Add items in new order
175
+ menu.append(start_item)
176
+ menu.append(stop_item)
177
+ menu.append(Gtk::SeparatorMenuItem.new)
178
+ menu.append(status_item)
179
+ menu.append(Gtk::SeparatorMenuItem.new)
180
+ menu.append(quit_item)
181
+
182
+ start_item.signal_connect("activate") do
183
+ set_icon(:running)
184
+ start_discourse
185
+ @running = true
186
+ end
94
187
 
95
- stop_item = Gtk::ImageMenuItem.new(label: "Stop Discourse")
96
- stop_item.image =
97
- Gtk::Image.new(icon_name: "media-playback-stop", size: :menu)
188
+ stop_item.signal_connect("activate") do
189
+ set_icon(:stopped)
190
+ stop_discourse
191
+ @running = false
192
+ end
98
193
 
99
- status_item = Gtk::ImageMenuItem.new(label: "Show Status")
100
- status_item.image =
101
- Gtk::Image.new(icon_name: "utilities-system-monitor", size: :menu)
194
+ quit_item.signal_connect("activate") do
195
+ cleanup
196
+ Gtk.main_quit
197
+ end
102
198
 
103
- quit_item = Gtk::ImageMenuItem.new(label: "Quit")
104
- quit_item.image =
105
- Gtk::Image.new(icon_name: "application-exit", size: :menu)
199
+ status_item.signal_connect("activate") { show_status_window }
106
200
 
107
- # Add items in new order
108
- menu.append(start_item)
109
- menu.append(stop_item)
110
- menu.append(Gtk::SeparatorMenuItem.new)
111
- menu.append(status_item)
112
- menu.append(Gtk::SeparatorMenuItem.new)
113
- menu.append(quit_item)
201
+ menu.show_all
114
202
 
115
- start_item.signal_connect("activate") do
116
- set_icon(:running)
117
- start_discourse
118
- @running = true
203
+ # Show/hide items based on running state - AFTER show_all
204
+ start_item.visible = !@running
205
+ stop_item.visible = @running
206
+ menu.popup(nil, nil, button, time)
119
207
  end
208
+ end
120
209
 
121
- stop_item.signal_connect("activate") do
122
- set_icon(:stopped)
123
- stop_discourse
124
- @running = false
125
- end
210
+ def start_discourse
211
+ @ember_output.clear
212
+ @unicorn_output.clear
126
213
 
127
- quit_item.signal_connect("activate") do
128
- cleanup
129
- Gtk.main_quit
214
+ Dir.chdir(@discourse_path) do
215
+ @processes[:ember] = start_process("bin/ember-cli")
216
+ @ember_running = true
217
+ @processes[:unicorn] = start_process("bin/unicorn")
218
+ @unicorn_running = true
219
+ update_tab_labels if @notebook
130
220
  end
221
+ end
131
222
 
132
- status_item.signal_connect("activate") { show_status_window }
133
-
134
- menu.show_all
135
-
136
- # Show/hide items based on running state - AFTER show_all
137
- start_item.visible = !@running
138
- stop_item.visible = @running
139
- menu.popup(nil, nil, button, time)
223
+ def stop_discourse
224
+ cleanup
140
225
  end
141
- end
142
226
 
143
- def start_discourse
144
- @ember_output.clear
145
- @unicorn_output.clear
227
+ def cleanup
228
+ return if @processes.empty?
146
229
 
147
- Dir.chdir(@discourse_path) do
148
- @processes[:ember] = start_process("bin/ember-cli")
149
- @ember_running = true
150
- @processes[:unicorn] = start_process("bin/unicorn")
151
- @unicorn_running = true
152
- update_tab_labels if @notebook
153
- end
154
- end
230
+ # First disable updates to prevent race conditions
231
+ @view_timeouts&.values&.each do |id|
232
+ begin
233
+ GLib::Source.remove(id)
234
+ rescue StandardError => e
235
+ puts "Error removing timeout: #{e}" if OPTIONS[:debug]
236
+ end
237
+ end
238
+ @view_timeouts&.clear
239
+
240
+ # Remove buffer trim timer if it exists
241
+ if @buffer_trim_timer
242
+ begin
243
+ GLib::Source.remove(@buffer_trim_timer)
244
+ @buffer_trim_timer = nil
245
+ rescue StandardError => e
246
+ puts "Error removing buffer trim timer: #{e}" if OPTIONS[:debug]
247
+ end
248
+ end
249
+
250
+ # Stop pipe thread
251
+ if @pipe_thread
252
+ begin
253
+ @pipe_queue.push(:exit) if @pipe_queue
254
+ @pipe_thread.join(2) # Wait up to 2 seconds
255
+ @pipe_thread.kill if @pipe_thread.alive?
256
+ rescue StandardError => e
257
+ puts "Error stopping pipe thread: #{e}" if OPTIONS[:debug]
258
+ end
259
+ end
155
260
 
156
- def stop_discourse
157
- cleanup
158
- end
261
+ # Then stop processes
262
+ @processes.each do |name, process|
263
+ begin
264
+ Process.kill("TERM", process[:pid])
265
+ # Wait for process to finish with timeout
266
+ Timeout.timeout(10) { process[:thread].join }
267
+ rescue StandardError => e
268
+ puts "Error stopping #{name}: #{e}" if OPTIONS[:debug]
269
+ end
270
+ end
271
+ @processes.clear
272
+ @ember_running = false
273
+ @unicorn_running = false
159
274
 
160
- def cleanup
161
- return if @processes.empty?
275
+ # Finally clean up UI elements
276
+ update_tab_labels if @notebook && !@notebook.destroyed?
162
277
 
163
- # First disable updates to prevent race conditions
164
- @view_timeouts&.values&.each do |id|
165
- begin
166
- GLib::Source.remove(id)
167
- rescue StandardError => e
168
- puts "Error removing timeout: #{e}" if OPTIONS[:debug]
278
+ if @status_window && !@status_window.destroyed?
279
+ @status_window.destroy
280
+ @status_window = nil
169
281
  end
170
282
  end
171
- @view_timeouts&.clear
172
283
 
173
- # Then stop processes
174
- @processes.each do |name, process|
284
+ def start_process(command, console: false)
285
+ puts "DEBUG: start_process called with command: #{command}" if OPTIONS[:debug]
286
+
287
+ return start_console_process(command) if console
288
+
175
289
  begin
176
- Process.kill("TERM", process[:pid])
177
- # Wait for process to finish with timeout
178
- Timeout.timeout(10) { process[:thread].join }
179
- rescue StandardError => e
180
- puts "Error stopping #{name}: #{e}" if OPTIONS[:debug]
290
+ stdin, stdout, stderr, wait_thr = Open3.popen3(command)
291
+ puts "DEBUG: Process started with PID: #{wait_thr.pid}" if OPTIONS[:debug]
292
+ rescue => e
293
+ puts "DEBUG: Error starting process: #{e.message}" if OPTIONS[:debug]
294
+ return nil
181
295
  end
182
- end
183
- @processes.clear
184
- @ember_running = false
185
- @unicorn_running = false
186
-
187
- # Finally clean up UI elements
188
- update_tab_labels if @notebook && !@notebook.destroyed?
189
296
 
190
- if @status_window && !@status_window.destroyed?
191
- @status_window.destroy
192
- @status_window = nil
193
- end
194
- end
297
+ # Create a monitor thread that will detect if process dies
298
+ monitor_thread =
299
+ Thread.new do
300
+ begin
301
+ wait_thr.value # Wait for process to finish
302
+ is_ember = command.include?("ember-cli")
303
+ @ember_running = false if is_ember
304
+ @unicorn_running = false unless is_ember
305
+ GLib::Idle.add do
306
+ update_tab_labels if @notebook
307
+ false
308
+ end
309
+ rescue => e
310
+ puts "DEBUG: Error in monitor thread: #{e.message}" if OPTIONS[:debug]
311
+ end
312
+ end
195
313
 
196
- def start_process(command)
197
- stdin, stdout, stderr, wait_thr = Open3.popen3(command)
314
+ # Clear the buffer before starting
315
+ buffer = command.include?("ember-cli") ? @ember_output : @unicorn_output
316
+ buffer.clear
317
+
318
+ # Add a start message to the buffer
319
+ timestamp = Time.now.strftime("%H:%M:%S")
320
+ buffer << "#{timestamp} - Starting #{command}...\n"
321
+
322
+ # Force immediate GUI update
323
+ GLib::Idle.add do
324
+ show_status_window if @status_window.nil? || !@status_window.visible?
325
+ update_all_views
326
+ false
327
+ end
198
328
 
199
- # Create a monitor thread that will detect if process dies
200
- monitor_thread =
329
+ # Monitor stdout
201
330
  Thread.new do
202
- wait_thr.value # Wait for process to finish
203
- is_ember = command.include?("ember-cli")
204
- @ember_running = false if is_ember
205
- @unicorn_running = false unless is_ember
206
- GLib::Idle.add do
207
- update_tab_labels if @notebook
208
- false
331
+ begin
332
+ while line = stdout.gets
333
+ buffer = command.include?("ember-cli") ? @ember_output : @unicorn_output
334
+ puts "[OUT] #{line}" if OPTIONS[:debug]
335
+
336
+ # Add to buffer with size management
337
+ buffer << line
338
+
339
+ # Print buffer size for debugging
340
+ if OPTIONS[:debug]
341
+ puts "DEBUG: Added to buffer: #{line.inspect}"
342
+ if buffer.size % 10 == 0
343
+ puts "DEBUG: Buffer size now: #{buffer.size}"
344
+ end
345
+ end
346
+
347
+ # Trim if needed
348
+ if buffer.size > BUFFER_SIZE
349
+ buffer.shift(buffer.size - BUFFER_SIZE)
350
+ end
351
+
352
+ # Force GUI update on main thread
353
+ GLib::Idle.add do
354
+ update_all_views
355
+ false
356
+ end
357
+
358
+ # Also publish to pipe for --attach mode in background
359
+ publish_to_pipe(line)
360
+ end
361
+ rescue => e
362
+ puts "DEBUG: Error in stdout thread: #{e.message}" if OPTIONS[:debug]
363
+ puts e.backtrace.join("\n") if OPTIONS[:debug]
364
+
365
+ # Add error to buffer for visibility
366
+ buffer = command.include?("ember-cli") ? @ember_output : @unicorn_output
367
+ error_msg = "ERROR: Exception in stdout thread: #{e.message}\n"
368
+ buffer << error_msg
369
+
370
+ # Force GUI update
371
+ GLib::Idle.add do
372
+ update_all_views
373
+ false
374
+ end
209
375
  end
210
376
  end
211
377
 
212
- # Monitor stdout
213
- Thread.new do
214
- while line = stdout.gets
215
- buffer = command.include?("ember-cli") ? @ember_output : @unicorn_output
216
- puts "[OUT] #{line}" if OPTIONS[:debug]
217
- buffer << line
218
- buffer.shift if buffer.size > BUFFER_SIZE
219
- # Force GUI update
220
- GLib::Idle.add do
221
- update_all_views
222
- false
378
+ # Monitor stderr
379
+ Thread.new do
380
+ begin
381
+ while line = stderr.gets
382
+ buffer = command.include?("ember-cli") ? @ember_output : @unicorn_output
383
+ puts "[ERR] #{line}" if OPTIONS[:debug]
384
+
385
+ # Format error line
386
+ error_line = "ERROR: #{line}"
387
+
388
+ # Add to buffer with size management
389
+ buffer << error_line
390
+
391
+ # Print buffer size for debugging
392
+ if OPTIONS[:debug]
393
+ puts "DEBUG: Added to buffer: #{error_line.inspect}"
394
+ if buffer.size % 10 == 0
395
+ puts "DEBUG: Buffer size now: #{buffer.size}"
396
+ end
397
+ end
398
+
399
+ # Trim if needed
400
+ if buffer.size > BUFFER_SIZE
401
+ buffer.shift(buffer.size - BUFFER_SIZE)
402
+ end
403
+
404
+ # Force GUI update on main thread
405
+ GLib::Idle.add do
406
+ update_all_views
407
+ false
408
+ end
409
+
410
+ # Also publish to pipe for --attach mode in background
411
+ publish_to_pipe(error_line)
412
+ end
413
+ rescue => e
414
+ puts "DEBUG: Error in stderr thread: #{e.message}" if OPTIONS[:debug]
415
+ puts e.backtrace.join("\n") if OPTIONS[:debug]
416
+
417
+ # Add error to buffer for visibility
418
+ buffer = command.include?("ember-cli") ? @ember_output : @unicorn_output
419
+ error_msg = "ERROR: Exception in stderr thread: #{e.message}\n"
420
+ buffer << error_msg
421
+
422
+ # Force GUI update
423
+ GLib::Idle.add do
424
+ update_all_views
425
+ false
426
+ end
223
427
  end
224
428
  end
429
+
430
+ {
431
+ pid: wait_thr.pid,
432
+ stdin: stdin,
433
+ stdout: stdout,
434
+ stderr: stderr,
435
+ thread: wait_thr,
436
+ monitor: monitor_thread
437
+ }
225
438
  end
226
439
 
227
- # Monitor stderr
228
- Thread.new do
229
- while line = stderr.gets
230
- buffer = command.include?("ember-cli") ? @ember_output : @unicorn_output
231
- puts "[ERR] #{line}" if OPTIONS[:debug]
232
- buffer << line
233
- buffer.shift if buffer.size > BUFFER_SIZE
234
- # Force GUI update
440
+ def show_status_window
441
+ puts "DEBUG: show_status_window called" if OPTIONS[:debug]
442
+
443
+ if @status_window&.visible?
444
+ puts "DEBUG: Status window already visible, presenting it" if OPTIONS[:debug]
445
+ @status_window.present
446
+ # Force window to current workspace in i3
447
+ if @status_window.window
448
+ @status_window.window.raise
449
+ if system("which i3-msg >/dev/null 2>&1")
450
+ # First move to current workspace, then focus
451
+ system(
452
+ "i3-msg '[id=#{@status_window.window.xid}] move workspace current'"
453
+ )
454
+ system("i3-msg '[id=#{@status_window.window.xid}] focus'")
455
+ end
456
+ end
457
+
458
+ # Force an update of the views even if window is already visible
235
459
  GLib::Idle.add do
236
460
  update_all_views
237
461
  false
238
462
  end
463
+
464
+ return
239
465
  end
240
- end
241
466
 
242
- {
243
- pid: wait_thr.pid,
244
- stdin: stdin,
245
- stdout: stdout,
246
- stderr: stderr,
247
- thread: wait_thr,
248
- monitor: monitor_thread
249
- }
250
- end
467
+ # Clean up any existing window
468
+ if @status_window
469
+ puts "DEBUG: Destroying existing status window" if OPTIONS[:debug]
470
+ @status_window.destroy
471
+ @status_window = nil
472
+ end
251
473
 
252
- def show_status_window
253
- if @status_window&.visible?
254
- @status_window.present
255
- # Force window to current workspace in i3
256
- if @status_window.window
257
- @status_window.window.raise
258
- if system("which i3-msg >/dev/null 2>&1")
259
- # First move to current workspace, then focus
260
- system(
261
- "i3-msg '[id=#{@status_window.window.xid}] move workspace current'"
262
- )
263
- system("i3-msg '[id=#{@status_window.window.xid}] focus'")
264
- end
474
+ puts "DEBUG: Creating new status window" if OPTIONS[:debug]
475
+ @status_window = Gtk::Window.new("Discourse Status")
476
+ @status_window.set_wmclass("discourse-status", "Discourse Status")
477
+
478
+ # Load saved geometry or use defaults
479
+ config = self.class.load_config
480
+ if config["window_geometry"]
481
+ geo = config["window_geometry"]
482
+ @status_window.move(geo["x"], geo["y"])
483
+ @status_window.resize(geo["width"], geo["height"])
484
+ puts "DEBUG: Set window geometry from config: #{geo.inspect}" if OPTIONS[:debug]
485
+ else
486
+ @status_window.set_default_size(800, 600)
487
+ @status_window.window_position = :center
488
+ puts "DEBUG: Set default window size 800x600" if OPTIONS[:debug]
489
+ end
490
+ @status_window.type_hint = :dialog
491
+ @status_window.set_role("discourse-status-dialog")
492
+
493
+ # Handle window destruction and hide
494
+ @status_window.signal_connect("delete-event") do
495
+ puts "DEBUG: Window delete-event triggered" if OPTIONS[:debug]
496
+ save_window_geometry
497
+ @status_window.hide
498
+ true # Prevent destruction
265
499
  end
266
- return
267
- end
268
500
 
269
- # Clean up any existing window
270
- if @status_window
271
- @status_window.destroy
272
- @status_window = nil
273
- end
501
+ # Save position and size when window is moved or resized
502
+ @status_window.signal_connect("configure-event") do
503
+ save_window_geometry
504
+ false
505
+ end
274
506
 
275
- @status_window = Gtk::Window.new("Discourse Status")
276
- @status_window.set_wmclass("discourse-status", "Discourse Status")
277
-
278
- # Load saved geometry or use defaults
279
- config = self.class.load_config
280
- if config["window_geometry"]
281
- geo = config["window_geometry"]
282
- @status_window.move(geo["x"], geo["y"])
283
- @status_window.resize(geo["width"], geo["height"])
284
- else
285
- @status_window.set_default_size(800, 600)
286
- @status_window.window_position = :center
507
+ puts "DEBUG: Creating notebook" if OPTIONS[:debug]
508
+ @notebook = Gtk::Notebook.new
509
+
510
+ # Debug buffer contents before creating views
511
+ puts "DEBUG: ember_output size: #{@ember_output.size}" if OPTIONS[:debug]
512
+ puts "DEBUG: unicorn_output size: #{@unicorn_output.size}" if OPTIONS[:debug]
513
+
514
+ puts "DEBUG: Creating ember view" if OPTIONS[:debug]
515
+ @ember_view = create_log_view(@ember_output)
516
+ @ember_label = create_status_label("Ember CLI", @ember_running)
517
+ @notebook.append_page(@ember_view, @ember_label)
518
+ puts "DEBUG: Added ember view to notebook" if OPTIONS[:debug]
519
+
520
+ puts "DEBUG: Creating unicorn view" if OPTIONS[:debug]
521
+ @unicorn_view = create_log_view(@unicorn_output)
522
+ @unicorn_label = create_status_label("Unicorn", @unicorn_running)
523
+ @notebook.append_page(@unicorn_view, @unicorn_label)
524
+ puts "DEBUG: Added unicorn view to notebook" if OPTIONS[:debug]
525
+
526
+ @status_window.add(@notebook)
527
+ puts "DEBUG: Added notebook to status window" if OPTIONS[:debug]
528
+
529
+ @status_window.show_all
530
+ puts "DEBUG: Called show_all on status window" if OPTIONS[:debug]
531
+
532
+ # Force an immediate update of the views
533
+ GLib::Idle.add do
534
+ puts "DEBUG: Forcing immediate update after window creation" if OPTIONS[:debug]
535
+ update_all_views
536
+ false
537
+ end
287
538
  end
288
- @status_window.type_hint = :dialog
289
- @status_window.set_role("discourse-status-dialog")
290
-
291
- # Handle window destruction and hide
292
- @status_window.signal_connect("delete-event") do
293
- save_window_geometry
294
- @status_window.hide
295
- true # Prevent destruction
539
+
540
+ def update_all_views
541
+ puts "DEBUG: update_all_views called" if OPTIONS[:debug]
542
+
543
+ # Basic validity checks
544
+ return unless @status_window && !@status_window.destroyed?
545
+ return unless @ember_view && @unicorn_view
546
+
547
+ begin
548
+ # Always update both views for now to ensure content is displayed
549
+ if @ember_view && !@ember_view.destroyed? && @ember_view.child && !@ember_view.child.destroyed?
550
+ update_log_view(@ember_view.child, @ember_output)
551
+ end
552
+
553
+ if @unicorn_view && !@unicorn_view.destroyed? && @unicorn_view.child && !@unicorn_view.child.destroyed?
554
+ update_log_view(@unicorn_view.child, @unicorn_output)
555
+ end
556
+
557
+ # Process any pending GTK events
558
+ while Gtk.events_pending?
559
+ Gtk.main_iteration_do(false)
560
+ end
561
+ rescue => e
562
+ puts "DEBUG: Error in update_all_views: #{e.message}" if OPTIONS[:debug]
563
+ puts e.backtrace.join("\n") if OPTIONS[:debug]
564
+ end
296
565
  end
297
566
 
298
- # Save position and size when window is moved or resized
299
- @status_window.signal_connect("configure-event") do
300
- save_window_geometry
301
- false
567
+ def create_log_view(buffer)
568
+ puts "DEBUG: create_log_view called for #{buffer == @ember_output ? 'ember' : 'unicorn'}" if OPTIONS[:debug]
569
+
570
+ # Create a scrolled window to contain the text view
571
+ scroll = Gtk::ScrolledWindow.new
572
+
573
+ # Create a simple text view with minimal configuration
574
+ text_view = Gtk::TextView.new
575
+ text_view.editable = false
576
+ text_view.cursor_visible = false
577
+ text_view.wrap_mode = :word
578
+
579
+ # Use a fixed-width font
580
+ text_view.monospace = true
581
+
582
+ # Set colors - white text on black background
583
+ text_view.override_background_color(:normal, Gdk::RGBA.new(0, 0, 0, 1))
584
+ text_view.override_color(:normal, Gdk::RGBA.new(1, 1, 1, 1))
585
+
586
+ # Set font size explicitly
587
+ font_desc = Pango::FontDescription.new
588
+ font_desc.family = "Monospace"
589
+ font_desc.size = 12 * Pango::SCALE
590
+ text_view.override_font(font_desc)
591
+
592
+ # Set initial text
593
+ text_view.buffer.text = "Loading log data...\n"
594
+
595
+ # Add the text view to the scrolled window
596
+ scroll.add(text_view)
597
+
598
+ # Set up a timer to update the view more frequently
599
+ @view_timeouts ||= {}
600
+ timeout_id = GLib::Timeout.add(250) do
601
+ if text_view.destroyed? || scroll.destroyed?
602
+ @view_timeouts.delete(text_view.object_id)
603
+ false # Stop the timer
604
+ else
605
+ begin
606
+ update_log_view(text_view, buffer)
607
+ rescue => e
608
+ puts "DEBUG: Error updating log view: #{e.message}" if OPTIONS[:debug]
609
+ end
610
+ true # Continue the timer
611
+ end
612
+ end
613
+
614
+ # Store the timeout ID for cleanup
615
+ @view_timeouts[text_view.object_id] = timeout_id
616
+
617
+ # Clean up when the view is destroyed
618
+ text_view.signal_connect("destroy") do
619
+ if id = @view_timeouts.delete(text_view.object_id)
620
+ GLib::Source.remove(id) rescue nil
621
+ end
622
+ end
623
+
624
+ # Do an initial update
625
+ update_log_view(text_view, buffer)
626
+
627
+ # Return the scrolled window
628
+ scroll
302
629
  end
303
630
 
304
- @notebook = Gtk::Notebook.new
631
+ # We're not using ANSI tags anymore since we're stripping ANSI codes
632
+
633
+ def update_log_view(text_view, buffer)
634
+ puts "DEBUG: update_log_view called" if OPTIONS[:debug]
635
+
636
+ # Basic validity checks
637
+ return if text_view.nil? || text_view.destroyed?
638
+ return if text_view.buffer.nil? || text_view.buffer.destroyed?
639
+
640
+ # Debug buffer content
641
+ if OPTIONS[:debug]
642
+ puts "DEBUG: Buffer size: #{buffer.size}"
643
+ if buffer.size > 0
644
+ puts "DEBUG: First line: #{buffer.first.inspect}"
645
+ puts "DEBUG: Last line: #{buffer.last.inspect}"
646
+ end
647
+ end
305
648
 
306
- @ember_view = create_log_view(@ember_output)
307
- @ember_label = create_status_label("Ember CLI", @ember_running)
308
- @notebook.append_page(@ember_view, @ember_label)
649
+ # If buffer is empty, add a placeholder message
650
+ if buffer.empty?
651
+ buffer << "No log data available yet. Start Discourse to see logs.\n"
652
+ end
309
653
 
310
- @unicorn_view = create_log_view(@unicorn_output)
311
- @unicorn_label = create_status_label("Unicorn", @unicorn_running)
312
- @notebook.append_page(@unicorn_view, @unicorn_label)
654
+ # Completely replace the buffer content with all lines
655
+ begin
656
+ # Make a local copy of the buffer to avoid race conditions
657
+ buffer_copy = buffer.dup
658
+
659
+ # Join all buffer lines into a single string
660
+ all_content = buffer_copy.join("")
661
+
662
+ # Strip ANSI codes
663
+ clean_content = all_content.gsub(/\e\[[0-9;]*[mK]/, '')
664
+
665
+ # Always update the content to ensure it's displayed
666
+ text_view.buffer.text = clean_content
667
+
668
+ puts "DEBUG: Updated buffer text (#{clean_content.length} chars)" if OPTIONS[:debug]
669
+
670
+ # Scroll to bottom
671
+ adj = text_view&.parent&.vadjustment
672
+ if adj
673
+ adj.value = adj.upper - adj.page_size
674
+ end
675
+
676
+ # Process any pending GTK events to ensure UI updates
677
+ while Gtk.events_pending?
678
+ Gtk.main_iteration_do(false)
679
+ end
680
+ rescue => e
681
+ puts "DEBUG: Error updating text view: #{e.message}" if OPTIONS[:debug]
682
+ puts e.backtrace.join("\n") if OPTIONS[:debug]
683
+
684
+ # Try a fallback approach
685
+ begin
686
+ text_view.buffer.text = "Error displaying log. See console for details.\n#{e.message}"
687
+ rescue => e2
688
+ puts "DEBUG: Even fallback approach failed: #{e2.message}" if OPTIONS[:debug]
689
+ end
690
+ end
691
+ end
313
692
 
314
- @status_window.add(@notebook)
315
- @status_window.show_all
316
- end
693
+ def create_status_label(text, running)
694
+ box = Gtk::Box.new(:horizontal, 5)
695
+ label = Gtk::Label.new(text)
696
+ status = Gtk::Label.new
697
+ color =
698
+ (
699
+ if running
700
+ Gdk::RGBA.new(0.2, 0.8, 0.2, 1)
701
+ else
702
+ Gdk::RGBA.new(0.8, 0.2, 0.2, 1)
703
+ end
704
+ )
705
+ status.override_color(:normal, color)
706
+ status.text = running ? "●" : "○"
707
+ box.pack_start(label, expand: false, fill: false, padding: 0)
708
+ box.pack_start(status, expand: false, fill: false, padding: 0)
709
+ box.show_all
710
+ box
711
+ end
317
712
 
318
- def update_all_views
319
- return unless @status_window && !@status_window.destroyed?
320
- return unless @ember_view && @unicorn_view
321
- return unless @ember_view.child && @unicorn_view.child
322
- return if @ember_view.destroyed? || @unicorn_view.destroyed?
323
- return if @ember_view.child.destroyed? || @unicorn_view.child.destroyed?
713
+ def update_tab_labels
714
+ return unless @notebook && !@notebook.destroyed?
715
+ return unless @ember_label && @unicorn_label
716
+ return if @ember_label.destroyed? || @unicorn_label.destroyed?
324
717
 
325
- begin
326
- if @ember_view.visible? && @ember_view.child.visible?
327
- update_log_view(@ember_view.child, @ember_output)
328
- end
329
- if @unicorn_view.visible? && @unicorn_view.child.visible?
330
- update_log_view(@unicorn_view.child, @unicorn_output)
718
+ [@ember_label, @unicorn_label].each do |label|
719
+ next unless label.children && label.children.length > 1
720
+ next if label.children[1].destroyed?
721
+
722
+ is_running = label == @ember_label ? @ember_running : @unicorn_running
723
+ begin
724
+ label.children[1].text = is_running ? "●" : "○"
725
+ label.children[1].override_color(
726
+ :normal,
727
+ Gdk::RGBA.new(
728
+ is_running ? 0.2 : 0.8,
729
+ is_running ? 0.8 : 0.2,
730
+ 0.2,
731
+ 1
732
+ )
733
+ )
734
+ rescue StandardError => e
735
+ puts "Error updating label: #{e}" if OPTIONS[:debug]
736
+ end
331
737
  end
332
- rescue StandardError => e
333
- puts "Error updating views: #{e}" if OPTIONS[:debug]
334
738
  end
335
- end
336
739
 
337
- def create_log_view(buffer)
338
- scroll = Gtk::ScrolledWindow.new
339
- text_view = Gtk::TextView.new
340
- text_view.editable = false
341
- text_view.wrap_mode = :word
740
+ def save_window_geometry
741
+ return unless @status_window&.visible? && @status_window.window
342
742
 
343
- # Set white text on black background
344
- text_view.override_background_color(:normal, Gdk::RGBA.new(0, 0, 0, 1))
345
- text_view.override_color(:normal, Gdk::RGBA.new(1, 1, 1, 1))
743
+ x, y = @status_window.position
744
+ width, height = @status_window.size
346
745
 
347
- # Create text tags for colors
348
- _tag_table = text_view.buffer.tag_table
349
- create_ansi_tags(text_view.buffer)
746
+ self.class.save_config(
747
+ path: @discourse_path,
748
+ window_geometry: {
749
+ "x" => x,
750
+ "y" => y,
751
+ "width" => width,
752
+ "height" => height
753
+ }
754
+ )
755
+ end
350
756
 
351
- # Initial text
352
- update_log_view(text_view, buffer)
757
+ def set_icon(status)
758
+ icon_file = status == :running ? "discourse_running.png" : "discourse.png"
759
+ icon_path = File.join(File.dirname(__FILE__), "../../assets", icon_file)
760
+ @indicator.pixbuf = GdkPixbuf::Pixbuf.new(file: icon_path)
761
+ end
353
762
 
354
- # Store timeouts in instance variable for proper cleanup
355
- @view_timeouts ||= {}
763
+ def start_console_process(command)
764
+ stdin, stdout, stderr, wait_thr = Open3.popen3(command)
356
765
 
357
- # Set up periodic refresh with validity check
358
- timeout_id =
359
- GLib::Timeout.add(1000) do
360
- if text_view&.parent.nil? || !text_view&.parent&.visible?
361
- @view_timeouts.delete(text_view.object_id)
362
- false # Stop the timeout if view is destroyed
363
- else
364
- begin
365
- update_log_view(text_view, buffer)
366
- rescue StandardError
367
- nil
368
- end
369
- true # Keep the timeout active
766
+ # Pipe stdout to console and add to buffer
767
+ Thread.new do
768
+ while line = stdout.gets
769
+ buffer =
770
+ command.include?("ember-cli") ? @ember_output : @unicorn_output
771
+ print line
772
+ buffer << line
773
+ buffer.shift if buffer.size > BUFFER_SIZE
370
774
  end
371
775
  end
372
776
 
373
- @view_timeouts[text_view.object_id] = timeout_id
374
-
375
- # Clean up timeout when view is destroyed
376
- text_view.signal_connect("destroy") do
377
- if timeout_id = @view_timeouts.delete(text_view.object_id)
378
- begin
379
- GLib::Source.remove(timeout_id)
380
- rescue StandardError
381
- nil
777
+ # Pipe stderr to console and add to buffer
778
+ Thread.new do
779
+ while line = stderr.gets
780
+ buffer =
781
+ command.include?("ember-cli") ? @ember_output : @unicorn_output
782
+ STDERR.print line
783
+ buffer << line
784
+ buffer.shift if buffer.size > BUFFER_SIZE
382
785
  end
383
786
  end
787
+
788
+ {
789
+ pid: wait_thr.pid,
790
+ stdin: stdin,
791
+ stdout: stdout,
792
+ stderr: stderr,
793
+ thread: wait_thr
794
+ }
384
795
  end
385
796
 
386
- scroll.add(text_view)
387
- scroll
388
- end
797
+ PIPE_PATH = "/tmp/discourse_systray_cmd"
798
+ PID_FILE = "/tmp/discourse_systray.pid"
389
799
 
390
- def create_ansi_tags(buffer)
391
- # Basic ANSI colors
392
- {
393
- "31" => "#ff6b6b", # Brighter red
394
- "32" => "#87ff87", # Brighter green
395
- "33" => "#ffff87", # Brighter yellow
396
- "34" => "#87d7ff", # Brighter blue
397
- "35" => "#ff87ff", # Brighter magenta
398
- "36" => "#87ffff", # Brighter cyan
399
- "37" => "#ffffff" # White
400
- }.each do |code, color|
401
- buffer.create_tag("ansi_#{code}", foreground: color)
800
+ def self.running?
801
+ return false unless File.exist?(PID_FILE)
802
+ pid = File.read(PID_FILE).to_i
803
+ Process.kill(0, pid)
804
+ true
805
+ rescue Errno::ESRCH, Errno::ENOENT
806
+ begin
807
+ File.unlink(PID_FILE)
808
+ rescue StandardError
809
+ nil
810
+ end
811
+ false
402
812
  end
403
813
 
404
- # Add more tags for bold, etc
405
- buffer.create_tag("bold", weight: :bold)
406
- end
814
+ attr_reader :discourse_path
815
+
816
+ def self.run
817
+ new.run
818
+ end
407
819
 
408
- def update_log_view(text_view, buffer)
409
- return if buffer.empty? || text_view.nil? || text_view.destroyed?
410
- return unless text_view.visible? && text_view.parent&.visible?
411
- return if text_view.buffer.nil? || text_view.buffer.destroyed?
412
-
413
- text_view.buffer.text = ""
414
- iter = text_view.buffer.get_iter_at(offset: 0)
415
-
416
- buffer.each do |line|
417
- # Parse ANSI sequences
418
- segments = line.scan(/\e\[([0-9;]*)m([^\e]*)|\e\[K([^\e]*)|([^\e]+)/)
419
-
420
- segments.each do |codes, text, clear_line, plain|
421
- if codes
422
- codes
423
- .split(";")
424
- .each do |code|
425
- case code
426
- when "1"
427
- text_view.buffer.apply_tag("bold", iter, iter)
428
- when "31".."37"
429
- text_view.buffer.apply_tag("ansi_#{code}", iter, iter)
820
+ def run
821
+ if OPTIONS[:attach]
822
+ require "rb-inotify"
823
+
824
+ # Initialize GTK for attach mode too
825
+ Gtk.init
826
+
827
+ # Initialize empty buffers
828
+ @ember_output = []
829
+ @unicorn_output = []
830
+
831
+ # Show status window immediately in attach mode too
832
+ GLib::Idle.add do
833
+ show_status_window
834
+ false
835
+ end
836
+
837
+ notifier = INotify::Notifier.new
838
+
839
+ begin
840
+ pipe = File.open(PIPE_PATH, "r")
841
+
842
+ # Watch for pipe deletion
843
+ notifier.watch(File.dirname(PIPE_PATH), :delete) do |event|
844
+ if event.name == File.basename(PIPE_PATH)
845
+ puts "Pipe was deleted, exiting."
846
+ exit 0
847
+ end
848
+ end
849
+
850
+ # Read from pipe in a separate thread
851
+ reader =
852
+ Thread.new do
853
+ begin
854
+ while true
855
+ begin
856
+ # Use non-blocking read with timeout
857
+ ready = IO.select([pipe], nil, nil, 0.1)
858
+ if ready && ready[0].include?(pipe)
859
+ line = pipe.gets
860
+ if line
861
+ puts line
862
+ STDOUT.flush
863
+
864
+ # Process the line for our buffers
865
+ if line.include?("ember") || line.include?("Ember") || line.include?("ERROR: ...")
866
+ @ember_output << line
867
+ puts "DEBUG: Added to ember buffer: #{line}" if OPTIONS[:debug]
868
+ else
869
+ @unicorn_output << line
870
+ puts "DEBUG: Added to unicorn buffer: #{line}" if OPTIONS[:debug]
871
+ end
872
+
873
+ # Force GUI update immediately
874
+ GLib::Idle.add do
875
+ update_all_views
876
+ false
877
+ end
878
+ end
879
+ end
880
+ rescue IOError, Errno::EBADF => e
881
+ puts "DEBUG: Pipe read error: #{e.message}" if OPTIONS[:debug]
882
+ break
883
+ end
884
+
885
+ # Check if pipe still exists
886
+ unless File.exist?(PIPE_PATH)
887
+ puts "Pipe was deleted, exiting."
888
+ exit 0
889
+ end
890
+
891
+ # Small sleep to prevent CPU hogging
892
+ sleep 0.01
893
+ end
894
+ rescue EOFError, IOError
895
+ puts "Pipe closed, exiting."
896
+ exit 0
430
897
  end
431
898
  end
432
- text_view.buffer.insert(iter, text)
433
- elsif clear_line
434
- text_view.buffer.insert(iter, clear_line)
435
- else
436
- text_view.buffer.insert(iter, plain || "")
899
+
900
+ # Start GTK main loop in a separate thread
901
+ gtk_thread = Thread.new do
902
+ Gtk.main
903
+ end
904
+
905
+ # Handle notifications in main thread
906
+ notifier.run
907
+ rescue Errno::ENOENT
908
+ puts "Pipe doesn't exist, exiting."
909
+ exit 1
910
+ ensure
911
+ reader&.kill
912
+ pipe&.close
913
+ notifier&.close
437
914
  end
438
- end
439
- end
915
+ else
916
+ return if self.class.running?
440
917
 
441
- # Scroll to bottom if near bottom
442
- if text_view&.parent&.vadjustment
443
- adj = text_view.parent.vadjustment
444
- if adj.value >= adj.upper - adj.page_size - 50
445
- adj.value = adj.upper - adj.page_size
446
- end
447
- end
448
- end
918
+ system("mkfifo #{PIPE_PATH}") unless File.exist?(PIPE_PATH)
449
919
 
450
- def create_status_label(text, running)
451
- box = Gtk::Box.new(:horizontal, 5)
452
- label = Gtk::Label.new(text)
453
- status = Gtk::Label.new
454
- color =
455
- (
456
- if running
457
- Gdk::RGBA.new(0.2, 0.8, 0.2, 1)
458
- else
459
- Gdk::RGBA.new(0.8, 0.2, 0.2, 1)
460
- end
461
- )
462
- status.override_color(:normal, color)
463
- status.text = running ? "●" : "○"
464
- box.pack_start(label, expand: false, fill: false, padding: 0)
465
- box.pack_start(status, expand: false, fill: false, padding: 0)
466
- box.show_all
467
- box
468
- end
920
+ # Create named pipe and write PID file
921
+ system("mkfifo #{PIPE_PATH}") unless File.exist?(PIPE_PATH)
922
+ File.write(PID_FILE, Process.pid.to_s)
469
923
 
470
- def update_tab_labels
471
- return unless @notebook
472
- @ember_label.children[1].text = @ember_running ? "●" : "○"
473
- @ember_label.children[1].override_color(
474
- :normal,
475
- Gdk::RGBA.new(
476
- @ember_running ? 0.2 : 0.8,
477
- @ember_running ? 0.8 : 0.2,
478
- 0.2,
479
- 1
480
- )
481
- )
482
-
483
- @unicorn_label.children[1].text = @unicorn_running ? "●" : "○"
484
- @unicorn_label.children[1].override_color(
485
- :normal,
486
- Gdk::RGBA.new(
487
- @unicorn_running ? 0.2 : 0.8,
488
- @unicorn_running ? 0.8 : 0.2,
489
- 0.2,
490
- 1
491
- )
492
- )
493
- end
924
+ # Set up cleanup on exit
925
+ at_exit do
926
+ begin
927
+ File.unlink(PIPE_PATH) if File.exist?(PIPE_PATH)
928
+ File.unlink(PID_FILE) if File.exist?(PID_FILE)
929
+ rescue StandardError => e
930
+ puts "Error during cleanup: #{e}" if OPTIONS[:debug]
931
+ end
932
+ end
494
933
 
495
- def save_window_geometry
496
- return unless @status_window&.visible? && @status_window.window
934
+ # Initialize GTK
935
+ Gtk.init
497
936
 
498
- x, y = @status_window.position
499
- width, height = @status_window.size
937
+ # Setup systray icon and menu
938
+ init_systray
939
+
940
+ # Show status window immediately on startup
941
+ GLib::Idle.add do
942
+ show_status_window
943
+ false
944
+ end
500
945
 
501
- self.class.save_config(
502
- path: @discourse_path,
503
- window_geometry: {
504
- "x" => x,
505
- "y" => y,
506
- "width" => width,
507
- "height" => height
508
- }
509
- )
510
- end
946
+ # Start GTK main loop
947
+ Gtk.main
948
+ end
949
+ end
511
950
 
512
- def set_icon(status)
513
- icon_file = status == :running ? "discourse_running.png" : "discourse.png"
514
- icon_path = File.join(File.dirname(__FILE__), "../../assets", icon_file)
515
- @indicator.pixbuf = GdkPixbuf::Pixbuf.new(file: icon_path)
516
- end
951
+ # Queue for pipe messages to avoid blocking
952
+ def initialize_pipe_queue
953
+ @pipe_queue = Queue.new
954
+ @pipe_thread = Thread.new do
955
+ loop do
956
+ begin
957
+ msg = @pipe_queue.pop
958
+ break if msg == :exit
959
+
960
+ if File.exist?(PIPE_PATH)
961
+ begin
962
+ # Use non-blocking write with timeout
963
+ Timeout.timeout(0.5) do
964
+ File.open(PIPE_PATH, "w") do |f|
965
+ f.puts(msg)
966
+ f.flush
967
+ end
968
+ end
969
+ rescue Timeout::Error
970
+ puts "Timeout writing to pipe" if OPTIONS[:debug]
971
+ rescue Errno::EPIPE, IOError => e
972
+ puts "Error writing to pipe: #{e}" if OPTIONS[:debug]
973
+ end
974
+ end
975
+ rescue => e
976
+ puts "Error in pipe thread: #{e}" if OPTIONS[:debug]
977
+ end
978
+
979
+ # Small sleep to prevent CPU hogging
980
+ sleep 0.01
981
+ end
982
+ end
983
+ end
984
+
985
+ def publish_to_pipe(msg)
986
+ puts "Publish to pipe: #{msg}" if OPTIONS[:debug]
987
+
988
+ # Add to our buffers directly - do this immediately
989
+ if msg.include?("ember") || msg.include?("Ember") || msg.include?("ERROR: ...")
990
+ @ember_output ||= []
991
+ @ember_output << msg
992
+ # Trim if needed
993
+ if @ember_output.size > BUFFER_SIZE
994
+ @ember_output.shift(@ember_output.size - BUFFER_SIZE)
995
+ end
996
+ else
997
+ @unicorn_output ||= []
998
+ @unicorn_output << msg
999
+ # Trim if needed
1000
+ if @unicorn_output.size > BUFFER_SIZE
1001
+ @unicorn_output.shift(@unicorn_output.size - BUFFER_SIZE)
1002
+ end
1003
+ end
1004
+
1005
+ # Force GUI update immediately
1006
+ GLib::Idle.add do
1007
+ update_all_views if defined?(update_all_views)
1008
+ false
1009
+ end
1010
+
1011
+ # Queue the message for pipe writing in background
1012
+ @pipe_queue.push(msg) if @pipe_queue
1013
+ end
517
1014
 
518
- def run
519
- Gtk.main
1015
+ def handle_command(cmd)
1016
+ puts "Received command: #{cmd}" if OPTIONS[:debug]
1017
+ end
520
1018
  end
521
1019
  end