discourse-systray 0.1.0
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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +56 -0
- data/assets/circle.png +0 -0
- data/assets/discourse.png +0 -0
- data/assets/discourse_running.png +0 -0
- data/assets/play.png +0 -0
- data/bin/discourse-systray +6 -0
- data/lib/discourse_systray/systray.rb +507 -0
- metadata +111 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 342570ecbf8771500b6be8dd6deba05ceb429b4465746c3fa2fc822dedbff311
|
4
|
+
data.tar.gz: c6a5dbc3ab88a256c2d9f5a9bda9def61ce26ec46a32280e4a81c144335f1017
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 183b3a72d39b1f6525d7a2ce533829349709192f8da6ede5d53ae231441a3064e37e04eaa94050ed3e63327784184c9b004049521c7a79fc6f2db688618aec96
|
7
|
+
data.tar.gz: 5c803012c9fdcaeea04465e8803e66f3531cec3c3487616256e24ede3b28d4e2a0e3a3e59c583fdb9a1a1618d411de98297b43b77ed7abd2c664e2a4a2e648de
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2024 Your Name
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
# Discourse Systray
|
2
|
+
|
3
|
+
A system tray application for managing local Discourse development instances.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
```bash
|
8
|
+
gem install discourse-systray
|
9
|
+
```
|
10
|
+
|
11
|
+
## Usage
|
12
|
+
|
13
|
+
Simply run:
|
14
|
+
|
15
|
+
```bash
|
16
|
+
discourse-systray
|
17
|
+
```
|
18
|
+
|
19
|
+
Optional flags:
|
20
|
+
- `--debug`: Enable debug mode
|
21
|
+
- `--path PATH`: Set Discourse path
|
22
|
+
|
23
|
+
### Features
|
24
|
+
|
25
|
+
- Start/Stop Discourse development environment from system tray
|
26
|
+
- Monitor Ember CLI and Unicorn server status
|
27
|
+
- View real-time logs with ANSI color support
|
28
|
+
- Status indicators for running services
|
29
|
+
- Clean process management and graceful shutdown
|
30
|
+
|
31
|
+
### System Tray Menu
|
32
|
+
|
33
|
+
- **Start Discourse**: Launches both Ember CLI and Unicorn server
|
34
|
+
- **Stop Discourse**: Gracefully stops all running processes
|
35
|
+
- **Show Status**: Opens log viewer window
|
36
|
+
- **Quit**: Exits application and cleans up processes
|
37
|
+
|
38
|
+
### Requirements
|
39
|
+
|
40
|
+
- Ruby >= 2.6.0
|
41
|
+
- GTK3
|
42
|
+
- Discourse development environment
|
43
|
+
|
44
|
+
## Development
|
45
|
+
|
46
|
+
After checking out the repo, run `bundle install` to install dependencies.
|
47
|
+
|
48
|
+
To install this gem onto your local machine, run:
|
49
|
+
```bash
|
50
|
+
gem build discourse-systray.gemspec
|
51
|
+
gem install ./discourse-systray-0.1.0.gem
|
52
|
+
```
|
53
|
+
|
54
|
+
## License
|
55
|
+
|
56
|
+
The gem is available as open source under the terms of the MIT License.
|
data/assets/circle.png
ADDED
Binary file
|
Binary file
|
Binary file
|
data/assets/play.png
ADDED
Binary file
|
@@ -0,0 +1,507 @@
|
|
1
|
+
require "gtk3"
|
2
|
+
require "open3"
|
3
|
+
require "optparse"
|
4
|
+
require "timeout"
|
5
|
+
require "fileutils"
|
6
|
+
require "json"
|
7
|
+
|
8
|
+
# Parse command line options
|
9
|
+
OPTIONS = { debug: false, path: nil }
|
10
|
+
|
11
|
+
OptionParser
|
12
|
+
.new do |opts|
|
13
|
+
opts.banner = "Usage: systray.rb [options]"
|
14
|
+
opts.on("--debug", "Enable debug mode") { OPTIONS[:debug] = true }
|
15
|
+
opts.on("--path PATH", "Set Discourse path") { |p| OPTIONS[:path] = p }
|
16
|
+
end
|
17
|
+
.parse!
|
18
|
+
|
19
|
+
class DiscourseSystemTray
|
20
|
+
CONFIG_DIR = File.expand_path("~/.config/discourse-systray")
|
21
|
+
CONFIG_FILE = File.join(CONFIG_DIR, "config.json")
|
22
|
+
|
23
|
+
def self.load_or_prompt_config
|
24
|
+
FileUtils.mkdir_p(CONFIG_DIR) unless Dir.exist?(CONFIG_DIR)
|
25
|
+
|
26
|
+
if OPTIONS[:path]
|
27
|
+
save_config(path: OPTIONS[:path])
|
28
|
+
return OPTIONS[:path]
|
29
|
+
end
|
30
|
+
|
31
|
+
if File.exist?(CONFIG_FILE)
|
32
|
+
config = JSON.parse(File.read(CONFIG_FILE))
|
33
|
+
return config["path"] if config["path"] && Dir.exist?(config["path"])
|
34
|
+
end
|
35
|
+
|
36
|
+
# Show dialog to get path
|
37
|
+
dialog =
|
38
|
+
Gtk::FileChooserDialog.new(
|
39
|
+
title: "Select Discourse Directory",
|
40
|
+
parent: nil,
|
41
|
+
action: :select_folder,
|
42
|
+
buttons: [["Cancel", :cancel], ["Select", :accept]]
|
43
|
+
)
|
44
|
+
|
45
|
+
path = nil
|
46
|
+
if dialog.run == :accept
|
47
|
+
path = dialog.filename
|
48
|
+
save_config(path: path)
|
49
|
+
else
|
50
|
+
puts "No Discourse path specified. Exiting."
|
51
|
+
exit 1
|
52
|
+
end
|
53
|
+
|
54
|
+
dialog.destroy
|
55
|
+
path
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.save_config(path:, window_geometry: nil)
|
59
|
+
config = { path: path }
|
60
|
+
config[:window_geometry] = window_geometry if window_geometry
|
61
|
+
File.write(CONFIG_FILE, JSON.generate(config))
|
62
|
+
nil # Prevent return value from being printed
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.load_config
|
66
|
+
return {} unless File.exist?(CONFIG_FILE)
|
67
|
+
JSON.parse(File.read(CONFIG_FILE))
|
68
|
+
rescue JSON::ParserError
|
69
|
+
{}
|
70
|
+
end
|
71
|
+
BUFFER_SIZE = 2000
|
72
|
+
|
73
|
+
def initialize
|
74
|
+
@discourse_path = self.class.load_or_prompt_config
|
75
|
+
@indicator = Gtk::StatusIcon.new
|
76
|
+
@indicator.pixbuf = GdkPixbuf::Pixbuf.new(file: File.join(File.dirname(__FILE__), "../../assets/discourse.png"))
|
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
|
85
|
+
|
86
|
+
# Create right-click menu
|
87
|
+
@indicator.signal_connect("popup-menu") do |tray, button, time|
|
88
|
+
menu = Gtk::Menu.new
|
89
|
+
|
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)
|
94
|
+
|
95
|
+
stop_item = Gtk::ImageMenuItem.new(label: "Stop Discourse")
|
96
|
+
stop_item.image =
|
97
|
+
Gtk::Image.new(icon_name: "media-playback-stop", size: :menu)
|
98
|
+
|
99
|
+
status_item = Gtk::ImageMenuItem.new(label: "Show Status")
|
100
|
+
status_item.image =
|
101
|
+
Gtk::Image.new(icon_name: "utilities-system-monitor", size: :menu)
|
102
|
+
|
103
|
+
quit_item = Gtk::ImageMenuItem.new(label: "Quit")
|
104
|
+
quit_item.image =
|
105
|
+
Gtk::Image.new(icon_name: "application-exit", size: :menu)
|
106
|
+
|
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)
|
114
|
+
|
115
|
+
start_item.signal_connect("activate") do
|
116
|
+
set_icon(:running)
|
117
|
+
start_discourse
|
118
|
+
@running = true
|
119
|
+
end
|
120
|
+
|
121
|
+
stop_item.signal_connect("activate") do
|
122
|
+
set_icon(:stopped)
|
123
|
+
stop_discourse
|
124
|
+
@running = false
|
125
|
+
end
|
126
|
+
|
127
|
+
quit_item.signal_connect("activate") do
|
128
|
+
cleanup
|
129
|
+
Gtk.main_quit
|
130
|
+
end
|
131
|
+
|
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)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def start_discourse
|
144
|
+
@ember_output.clear
|
145
|
+
@unicorn_output.clear
|
146
|
+
|
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
|
155
|
+
|
156
|
+
def stop_discourse
|
157
|
+
cleanup
|
158
|
+
end
|
159
|
+
|
160
|
+
def cleanup
|
161
|
+
return if @processes.empty?
|
162
|
+
|
163
|
+
@processes.each do |name, process|
|
164
|
+
begin
|
165
|
+
Process.kill("TERM", process[:pid])
|
166
|
+
# Wait for process to finish with timeout
|
167
|
+
Timeout.timeout(10) { process[:thread].join }
|
168
|
+
rescue StandardError => e
|
169
|
+
puts "Error stopping #{name}: #{e}" if OPTIONS[:debug]
|
170
|
+
end
|
171
|
+
end
|
172
|
+
@processes.clear
|
173
|
+
@ember_running = false
|
174
|
+
@unicorn_running = false
|
175
|
+
update_tab_labels if @notebook
|
176
|
+
|
177
|
+
# Clean up window and timeouts on exit
|
178
|
+
if @status_window
|
179
|
+
# Clean up any existing timeouts
|
180
|
+
if @view_timeouts
|
181
|
+
@view_timeouts.values.each do |id|
|
182
|
+
begin
|
183
|
+
GLib::Source.remove(id)
|
184
|
+
rescue StandardError
|
185
|
+
nil
|
186
|
+
end
|
187
|
+
end
|
188
|
+
@view_timeouts.clear
|
189
|
+
end
|
190
|
+
@status_window.destroy
|
191
|
+
@status_window = nil
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def start_process(command)
|
196
|
+
stdin, stdout, stderr, wait_thr = Open3.popen3(command)
|
197
|
+
|
198
|
+
# Create a monitor thread that will detect if process dies
|
199
|
+
monitor_thread =
|
200
|
+
Thread.new do
|
201
|
+
wait_thr.value # Wait for process to finish
|
202
|
+
is_ember = command.include?("ember-cli")
|
203
|
+
@ember_running = false if is_ember
|
204
|
+
@unicorn_running = false unless is_ember
|
205
|
+
GLib::Idle.add do
|
206
|
+
update_tab_labels if @notebook
|
207
|
+
false
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
# Monitor stdout
|
212
|
+
Thread.new do
|
213
|
+
while line = stdout.gets
|
214
|
+
buffer = command.include?("ember-cli") ? @ember_output : @unicorn_output
|
215
|
+
puts "[OUT] #{line}" if OPTIONS[:debug]
|
216
|
+
buffer << line
|
217
|
+
buffer.shift if buffer.size > BUFFER_SIZE
|
218
|
+
# Force GUI update
|
219
|
+
GLib::Idle.add do
|
220
|
+
update_all_views
|
221
|
+
false
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
# Monitor stderr
|
227
|
+
Thread.new do
|
228
|
+
while line = stderr.gets
|
229
|
+
buffer = command.include?("ember-cli") ? @ember_output : @unicorn_output
|
230
|
+
puts "[ERR] #{line}" if OPTIONS[:debug]
|
231
|
+
buffer << line
|
232
|
+
buffer.shift if buffer.size > BUFFER_SIZE
|
233
|
+
# Force GUI update
|
234
|
+
GLib::Idle.add do
|
235
|
+
update_all_views
|
236
|
+
false
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
{
|
242
|
+
pid: wait_thr.pid,
|
243
|
+
stdin: stdin,
|
244
|
+
stdout: stdout,
|
245
|
+
stderr: stderr,
|
246
|
+
thread: wait_thr,
|
247
|
+
monitor: monitor_thread
|
248
|
+
}
|
249
|
+
end
|
250
|
+
|
251
|
+
def show_status_window
|
252
|
+
if @status_window&.visible?
|
253
|
+
@status_window.present
|
254
|
+
# Force window to current workspace in i3
|
255
|
+
if @status_window.window
|
256
|
+
@status_window.window.raise
|
257
|
+
if system("which i3-msg >/dev/null 2>&1")
|
258
|
+
# First move to current workspace, then focus
|
259
|
+
system(
|
260
|
+
"i3-msg '[id=#{@status_window.window.xid}] move workspace current'"
|
261
|
+
)
|
262
|
+
system("i3-msg '[id=#{@status_window.window.xid}] focus'")
|
263
|
+
end
|
264
|
+
end
|
265
|
+
return
|
266
|
+
end
|
267
|
+
|
268
|
+
# Clean up any existing window
|
269
|
+
if @status_window
|
270
|
+
@status_window.destroy
|
271
|
+
@status_window = nil
|
272
|
+
end
|
273
|
+
|
274
|
+
@status_window = Gtk::Window.new("Discourse Status")
|
275
|
+
@status_window.set_wmclass("discourse-status", "Discourse Status")
|
276
|
+
|
277
|
+
# Load saved geometry or use defaults
|
278
|
+
config = self.class.load_config
|
279
|
+
if config["window_geometry"]
|
280
|
+
geo = config["window_geometry"]
|
281
|
+
@status_window.move(geo["x"], geo["y"])
|
282
|
+
@status_window.resize(geo["width"], geo["height"])
|
283
|
+
else
|
284
|
+
@status_window.set_default_size(800, 600)
|
285
|
+
@status_window.window_position = :center
|
286
|
+
end
|
287
|
+
@status_window.type_hint = :dialog
|
288
|
+
@status_window.set_role("discourse-status-dialog")
|
289
|
+
|
290
|
+
# Handle window destruction and hide
|
291
|
+
@status_window.signal_connect("delete-event") do
|
292
|
+
save_window_geometry
|
293
|
+
@status_window.hide
|
294
|
+
true # Prevent destruction
|
295
|
+
end
|
296
|
+
|
297
|
+
# Save position and size when window is moved or resized
|
298
|
+
@status_window.signal_connect("configure-event") do
|
299
|
+
save_window_geometry
|
300
|
+
false
|
301
|
+
end
|
302
|
+
|
303
|
+
@notebook = Gtk::Notebook.new
|
304
|
+
|
305
|
+
@ember_view = create_log_view(@ember_output)
|
306
|
+
@ember_label = create_status_label("Ember CLI", @ember_running)
|
307
|
+
@notebook.append_page(@ember_view, @ember_label)
|
308
|
+
|
309
|
+
@unicorn_view = create_log_view(@unicorn_output)
|
310
|
+
@unicorn_label = create_status_label("Unicorn", @unicorn_running)
|
311
|
+
@notebook.append_page(@unicorn_view, @unicorn_label)
|
312
|
+
|
313
|
+
@status_window.add(@notebook)
|
314
|
+
@status_window.show_all
|
315
|
+
end
|
316
|
+
|
317
|
+
def update_all_views
|
318
|
+
return unless @ember_view && @unicorn_view
|
319
|
+
|
320
|
+
update_log_view(@ember_view.child, @ember_output)
|
321
|
+
update_log_view(@unicorn_view.child, @unicorn_output)
|
322
|
+
end
|
323
|
+
|
324
|
+
def create_log_view(buffer)
|
325
|
+
scroll = Gtk::ScrolledWindow.new
|
326
|
+
text_view = Gtk::TextView.new
|
327
|
+
text_view.editable = false
|
328
|
+
text_view.wrap_mode = :word
|
329
|
+
|
330
|
+
# Set white text on black background
|
331
|
+
text_view.override_background_color(:normal, Gdk::RGBA.new(0, 0, 0, 1))
|
332
|
+
text_view.override_color(:normal, Gdk::RGBA.new(1, 1, 1, 1))
|
333
|
+
|
334
|
+
# Create text tags for colors
|
335
|
+
_tag_table = text_view.buffer.tag_table
|
336
|
+
create_ansi_tags(text_view.buffer)
|
337
|
+
|
338
|
+
# Initial text
|
339
|
+
update_log_view(text_view, buffer)
|
340
|
+
|
341
|
+
# Store timeouts in instance variable for proper cleanup
|
342
|
+
@view_timeouts ||= {}
|
343
|
+
|
344
|
+
# Set up periodic refresh with validity check
|
345
|
+
timeout_id =
|
346
|
+
GLib::Timeout.add(1000) do
|
347
|
+
if text_view&.parent.nil? || !text_view&.parent&.visible?
|
348
|
+
@view_timeouts.delete(text_view.object_id)
|
349
|
+
false # Stop the timeout if view is destroyed
|
350
|
+
else
|
351
|
+
begin
|
352
|
+
update_log_view(text_view, buffer)
|
353
|
+
rescue StandardError
|
354
|
+
nil
|
355
|
+
end
|
356
|
+
true # Keep the timeout active
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
@view_timeouts[text_view.object_id] = timeout_id
|
361
|
+
|
362
|
+
# Clean up timeout when view is destroyed
|
363
|
+
text_view.signal_connect("destroy") do
|
364
|
+
if timeout_id = @view_timeouts.delete(text_view.object_id)
|
365
|
+
begin
|
366
|
+
GLib::Source.remove(timeout_id)
|
367
|
+
rescue StandardError
|
368
|
+
nil
|
369
|
+
end
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
scroll.add(text_view)
|
374
|
+
scroll
|
375
|
+
end
|
376
|
+
|
377
|
+
def create_ansi_tags(buffer)
|
378
|
+
# Basic ANSI colors
|
379
|
+
{
|
380
|
+
"31" => "#ff6b6b", # Brighter red
|
381
|
+
"32" => "#87ff87", # Brighter green
|
382
|
+
"33" => "#ffff87", # Brighter yellow
|
383
|
+
"34" => "#87d7ff", # Brighter blue
|
384
|
+
"35" => "#ff87ff", # Brighter magenta
|
385
|
+
"36" => "#87ffff", # Brighter cyan
|
386
|
+
"37" => "#ffffff" # White
|
387
|
+
}.each do |code, color|
|
388
|
+
buffer.create_tag("ansi_#{code}", foreground: color)
|
389
|
+
end
|
390
|
+
|
391
|
+
# Add more tags for bold, etc
|
392
|
+
buffer.create_tag("bold", weight: :bold)
|
393
|
+
end
|
394
|
+
|
395
|
+
def update_log_view(text_view, buffer)
|
396
|
+
return if buffer.empty? || text_view.nil? || !text_view.visible?
|
397
|
+
return unless text_view.parent&.visible?
|
398
|
+
|
399
|
+
text_view.buffer.text = ""
|
400
|
+
iter = text_view.buffer.get_iter_at(offset: 0)
|
401
|
+
|
402
|
+
buffer.each do |line|
|
403
|
+
# Parse ANSI sequences
|
404
|
+
segments = line.scan(/\e\[([0-9;]*)m([^\e]*)|\e\[K([^\e]*)|([^\e]+)/)
|
405
|
+
|
406
|
+
segments.each do |codes, text, clear_line, plain|
|
407
|
+
if codes
|
408
|
+
codes
|
409
|
+
.split(";")
|
410
|
+
.each do |code|
|
411
|
+
case code
|
412
|
+
when "1"
|
413
|
+
text_view.buffer.apply_tag("bold", iter, iter)
|
414
|
+
when "31".."37"
|
415
|
+
text_view.buffer.apply_tag("ansi_#{code}", iter, iter)
|
416
|
+
end
|
417
|
+
end
|
418
|
+
text_view.buffer.insert(iter, text)
|
419
|
+
elsif clear_line
|
420
|
+
text_view.buffer.insert(iter, clear_line)
|
421
|
+
else
|
422
|
+
text_view.buffer.insert(iter, plain || "")
|
423
|
+
end
|
424
|
+
end
|
425
|
+
end
|
426
|
+
|
427
|
+
# Scroll to bottom if near bottom
|
428
|
+
if text_view&.parent&.vadjustment
|
429
|
+
adj = text_view.parent.vadjustment
|
430
|
+
if adj.value >= adj.upper - adj.page_size - 50
|
431
|
+
adj.value = adj.upper - adj.page_size
|
432
|
+
end
|
433
|
+
end
|
434
|
+
end
|
435
|
+
|
436
|
+
def create_status_label(text, running)
|
437
|
+
box = Gtk::Box.new(:horizontal, 5)
|
438
|
+
label = Gtk::Label.new(text)
|
439
|
+
status = Gtk::Label.new
|
440
|
+
color =
|
441
|
+
(
|
442
|
+
if running
|
443
|
+
Gdk::RGBA.new(0.2, 0.8, 0.2, 1)
|
444
|
+
else
|
445
|
+
Gdk::RGBA.new(0.8, 0.2, 0.2, 1)
|
446
|
+
end
|
447
|
+
)
|
448
|
+
status.override_color(:normal, color)
|
449
|
+
status.text = running ? "●" : "○"
|
450
|
+
box.pack_start(label, expand: false, fill: false, padding: 0)
|
451
|
+
box.pack_start(status, expand: false, fill: false, padding: 0)
|
452
|
+
box.show_all
|
453
|
+
box
|
454
|
+
end
|
455
|
+
|
456
|
+
def update_tab_labels
|
457
|
+
return unless @notebook
|
458
|
+
@ember_label.children[1].text = @ember_running ? "●" : "○"
|
459
|
+
@ember_label.children[1].override_color(
|
460
|
+
:normal,
|
461
|
+
Gdk::RGBA.new(
|
462
|
+
@ember_running ? 0.2 : 0.8,
|
463
|
+
@ember_running ? 0.8 : 0.2,
|
464
|
+
0.2,
|
465
|
+
1
|
466
|
+
)
|
467
|
+
)
|
468
|
+
|
469
|
+
@unicorn_label.children[1].text = @unicorn_running ? "●" : "○"
|
470
|
+
@unicorn_label.children[1].override_color(
|
471
|
+
:normal,
|
472
|
+
Gdk::RGBA.new(
|
473
|
+
@unicorn_running ? 0.2 : 0.8,
|
474
|
+
@unicorn_running ? 0.8 : 0.2,
|
475
|
+
0.2,
|
476
|
+
1
|
477
|
+
)
|
478
|
+
)
|
479
|
+
end
|
480
|
+
|
481
|
+
def save_window_geometry
|
482
|
+
return unless @status_window&.visible? && @status_window.window
|
483
|
+
|
484
|
+
x, y = @status_window.position
|
485
|
+
width, height = @status_window.size
|
486
|
+
|
487
|
+
self.class.save_config(
|
488
|
+
path: @discourse_path,
|
489
|
+
window_geometry: {
|
490
|
+
"x" => x,
|
491
|
+
"y" => y,
|
492
|
+
"width" => width,
|
493
|
+
"height" => height
|
494
|
+
}
|
495
|
+
)
|
496
|
+
end
|
497
|
+
|
498
|
+
def set_icon(status)
|
499
|
+
icon_file = status == :running ? "discourse_running.png" : "discourse.png"
|
500
|
+
icon_path = File.join(File.dirname(__FILE__), "../../assets", icon_file)
|
501
|
+
@indicator.pixbuf = GdkPixbuf::Pixbuf.new(file: icon_path)
|
502
|
+
end
|
503
|
+
|
504
|
+
def run
|
505
|
+
Gtk.main
|
506
|
+
end
|
507
|
+
end
|
metadata
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: discourse-systray
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Sam Saffron
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-01-23 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: gtk3
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.2'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4.2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '13.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '13.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.0'
|
69
|
+
description: A GTK3 system tray application that helps manage local Discourse development
|
70
|
+
instances
|
71
|
+
email:
|
72
|
+
- sam.saffron@gmail.com
|
73
|
+
executables:
|
74
|
+
- discourse-systray
|
75
|
+
extensions: []
|
76
|
+
extra_rdoc_files: []
|
77
|
+
files:
|
78
|
+
- LICENSE.txt
|
79
|
+
- README.md
|
80
|
+
- assets/circle.png
|
81
|
+
- assets/discourse.png
|
82
|
+
- assets/discourse_running.png
|
83
|
+
- assets/play.png
|
84
|
+
- bin/discourse-systray
|
85
|
+
- lib/discourse_systray/systray.rb
|
86
|
+
homepage: https://github.com/SamSaffron/discourse-systray
|
87
|
+
licenses:
|
88
|
+
- MIT
|
89
|
+
metadata:
|
90
|
+
homepage_uri: https://github.com/SamSaffron/discourse-systray
|
91
|
+
source_code_uri: https://github.com/SamSaffron/discourse-systray
|
92
|
+
post_install_message:
|
93
|
+
rdoc_options: []
|
94
|
+
require_paths:
|
95
|
+
- lib
|
96
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
97
|
+
requirements:
|
98
|
+
- - ">="
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: 3.0.0
|
101
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
102
|
+
requirements:
|
103
|
+
- - ">="
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: '0'
|
106
|
+
requirements: []
|
107
|
+
rubygems_version: 3.5.22
|
108
|
+
signing_key:
|
109
|
+
specification_version: 4
|
110
|
+
summary: System tray application for managing Discourse development
|
111
|
+
test_files: []
|