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.
- checksums.yaml +4 -4
- data/bin/discourse-systray +1 -1
- data/lib/discourse_systray/systray.rb +921 -423
- metadata +16 -2
@@ -5,517 +5,1015 @@ require "timeout"
|
|
5
5
|
require "fileutils"
|
6
6
|
require "json"
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
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
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
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
|
-
|
52
|
-
|
53
|
-
|
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
|
-
|
56
|
-
|
57
|
-
|
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
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
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
|
-
|
87
|
-
|
88
|
-
|
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
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
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
|
-
|
96
|
-
|
97
|
-
|
188
|
+
stop_item.signal_connect("activate") do
|
189
|
+
set_icon(:stopped)
|
190
|
+
stop_discourse
|
191
|
+
@running = false
|
192
|
+
end
|
98
193
|
|
99
|
-
|
100
|
-
|
101
|
-
|
194
|
+
quit_item.signal_connect("activate") do
|
195
|
+
cleanup
|
196
|
+
Gtk.main_quit
|
197
|
+
end
|
102
198
|
|
103
|
-
|
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
|
-
|
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
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
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
|
-
|
122
|
-
|
123
|
-
|
124
|
-
@running = false
|
125
|
-
end
|
210
|
+
def start_discourse
|
211
|
+
@ember_output.clear
|
212
|
+
@unicorn_output.clear
|
126
213
|
|
127
|
-
|
128
|
-
|
129
|
-
|
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
|
-
|
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
|
-
|
144
|
-
|
145
|
-
@unicorn_output.clear
|
227
|
+
def cleanup
|
228
|
+
return if @processes.empty?
|
146
229
|
|
147
|
-
|
148
|
-
@
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
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
|
-
|
157
|
-
|
158
|
-
|
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
|
-
|
161
|
-
|
275
|
+
# Finally clean up UI elements
|
276
|
+
update_tab_labels if @notebook && !@notebook.destroyed?
|
162
277
|
|
163
|
-
|
164
|
-
|
165
|
-
|
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
|
-
|
174
|
-
|
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
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
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
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
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
|
-
|
197
|
-
|
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
|
-
|
200
|
-
monitor_thread =
|
329
|
+
# Monitor stdout
|
201
330
|
Thread.new do
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
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
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
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
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
puts "
|
232
|
-
|
233
|
-
|
234
|
-
|
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
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
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
|
-
|
253
|
-
|
254
|
-
@status_window.
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
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
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
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
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
@
|
284
|
-
|
285
|
-
@
|
286
|
-
|
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
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
@
|
295
|
-
|
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
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
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
|
-
|
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
|
-
|
307
|
-
|
308
|
-
|
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
|
-
|
311
|
-
|
312
|
-
|
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
|
-
|
315
|
-
|
316
|
-
|
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
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
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
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
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
|
-
|
338
|
-
|
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
|
-
|
344
|
-
|
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
|
-
|
348
|
-
|
349
|
-
|
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
|
-
|
352
|
-
|
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
|
-
|
355
|
-
|
763
|
+
def start_console_process(command)
|
764
|
+
stdin, stdout, stderr, wait_thr = Open3.popen3(command)
|
356
765
|
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
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
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
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
|
-
|
387
|
-
|
388
|
-
end
|
797
|
+
PIPE_PATH = "/tmp/discourse_systray_cmd"
|
798
|
+
PID_FILE = "/tmp/discourse_systray.pid"
|
389
799
|
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
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
|
-
|
405
|
-
|
406
|
-
|
814
|
+
attr_reader :discourse_path
|
815
|
+
|
816
|
+
def self.run
|
817
|
+
new.run
|
818
|
+
end
|
407
819
|
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
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
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
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
|
-
|
439
|
-
|
915
|
+
else
|
916
|
+
return if self.class.running?
|
440
917
|
|
441
|
-
|
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
|
-
|
451
|
-
|
452
|
-
|
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
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
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
|
-
|
496
|
-
|
934
|
+
# Initialize GTK
|
935
|
+
Gtk.init
|
497
936
|
|
498
|
-
|
499
|
-
|
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
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
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
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
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
|
-
|
519
|
-
|
1015
|
+
def handle_command(cmd)
|
1016
|
+
puts "Received command: #{cmd}" if OPTIONS[:debug]
|
1017
|
+
end
|
520
1018
|
end
|
521
1019
|
end
|