"lib/git@gitlab.james.tl:james/dotfiles.git" did not exist on "f657777f1ba73ee1a3023740dba792e1662bbc73"
status.rb.erb 10.1 KB
Newer Older
1
require 'concurrent'
2
require 'fileutils'
3
require 'json'
4
5
6
7

class StatusWidget
  STATUS_FIFO_DIR = "/tmp/status-#{Process.uid}".freeze

8
  def initialize(name, timeout = nil)
9
10
11
12
    @fifo_name = File.join(STATUS_FIFO_DIR, "#{name}.fifo")
    @timeout = timeout
  end

13
14
15
16
17
18
  def run
    monitor(true)
  end

  def monitor(stdout = false)
    at_exit { cleanup }
19
    @output = stdout ? STDOUT : mkfifo(@fifo_name)
20
    Signal.trap('USR1') { trigger }
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
    puts segments
    start_timer
    loop do
      start_monitor
      sleep 1
    end
  end

  protected

  def trigger
    stop_timer
    puts segments
    start_timer
  end

  def fontawesome(char)
    "<span font='FontAwesome 5 Free Solid'>#{char}</span>"
  end

  def segment(str, first = false)
James T. Lee's avatar
James T. Lee committed
42
    content = colorize('#d8d8d8', '#282828', " #{str} ")
43
    if first
James T. Lee's avatar
James T. Lee committed
44
      "#{colorize '#282828', '#000000', ''}#{content}"
45
46
47
48
49
    else
      "#{colorize '#000000', '#282828', ''}#{content}"
    end
  end

James T. Lee's avatar
James T. Lee committed
50
51
52
53
54
55
56
  def colorize(fg, bg, text, bold=false)
    fgcolor=" fgcolor='#{fg}'" if fg
    bgcolor=" bgcolor='#{bg}'" if bg
    font_weight=" font_weight='bold'" if bold
    "<span#{fgcolor}#{bgcolor}#{font_weight}>#{text}</span>"
  end

57
  def puts(str)
58
59
60
61
62
63
64
    if str
      @output.puts(str)
      @output.flush
    end
  end

  def mkfifo(fifo_name)
65
66
67
68
69
70
    begin
      Dir.mkdir STATUS_FIFO_DIR
    rescue SystemCallError => e
      raise unless e.errno == Errno::EEXIST::Errno
    end

71
72
    File.mkfifo(fifo_name, 0600) unless File.exist? fifo_name
    open(fifo_name, 'w+')
73
74
75
76
  end

  private

77
78
79
80
  def cleanup
    stop_timer
    @output.close if @output
    File.unlink @fifo_name if File.exist? @fifo_name
81
82
83
84
85
86

    begin
      FileUtils.rmdir STATUS_FIFO_DIR
    rescue SystemCallError => e
      raise unless [Errno::ENOTEMPTY::Errno, Errno::ENOENT::Errno].include? e.errno
    end
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
  end

  def start_timer
    if @timeout
      @timer = Concurrent::TimerTask.new(execution_interval: @timeout) { puts segments }
      @timer.execute
    end
  end

  def stop_timer
    if @timer
      @timer.shutdown
      @timer = nil
    end
  end
end

class LoadStatus < StatusWidget
105
106
  def initialize
    super('load', 1)
107
108
109
110
111
112
113
114
115
  end

  private

  def start_monitor
    sleep
  end

  def segments
116
117
    loadavg = File.read('/proc/loadavg').split(' ').first
    segment("#{fontawesome "\uf233"} #{loadavg}", first: true)
118
119
120
121
  end
end

class PowerStatus < StatusWidget
122
123
  def initialize
    super('power', 5)
124
125
126
127
128
129
130
131
132
133
134
135
136
137
  end

  private

  def start_monitor
    @p = IO.popen('upower --monitor')
    @p.each do |line|
      if line !~ /^Monitoring/
        trigger
      end
    end
    @p.close
  end

138
  def cleanup
139
    Process.kill('TERM', @p.pid) if @p and !@p.closed?
140
141
142
    super
  end

143
144
145
146
147
148
149
150
151
152
  def segments
    `upower --enumerate`.lines.map do |line|
      if line =~ /\/battery_/
        battery_object = line
        state = nil; percentage = nil; tte = '...'
        `upower --show-info #{battery_object}`.each_line do |info_line|
          if info_line =~ /state:\s+(.*)/
            state = $1
          elsif info_line =~ /percentage:\s+(.*)%/
            percentage = $1.to_i
153
          elsif info_line =~ /time to empty:\s+(.*)\s(\w)/
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
            tte = "#{$1}#{$2}"
          end
        end

        case state
        when 'discharging'
          segment "#{battery_icon(percentage)} #{tte}"
        when 'charging'
          segment "#{fontawesome "\uf0e7"} #{percentage}%"
        else
          ''
        end
      else
        ''
      end
    end.join
  end

  def battery_icon(percentage)
    if percentage > 80
      fontawesome "\uf240"
    elsif percentage > 60
      fontawesome "\uf241"
    elsif percentage > 40
      fontawesome "\uf242"
    elsif percentage > 20
      fontawesome "\uf243"
    else
      fontawesome "\uf244"
    end
  end
end

class NetworkStatus < StatusWidget
188
189
  def initialize
    super('network', 5)
190
191
192
193
194
195
196
197
198
199
  end

  private

  def start_monitor
    @p = IO.popen('nmcli device monitor')
    @p.each { trigger }
    @p.close
  end

200
  def cleanup
201
    Process.kill('TERM', @p.pid) if @p and !@p.closed?
202
203
204
    super
  end

205
  def segments
206
    (device, type, devstate, connection) = `nmcli --colors no --terse device status`.lines.first.chomp.split(':')
207
208
209
210
211
212

    if type == 'wifi'
      icon = fontawesome "\uf1eb"

      case devstate
      when 'connected'
213
214
215
216
217
        if `iw dev #{device} link` =~ /signal:\s+(\S+)\s(\S+)/
          signal = "#{$1}#{$2}"
          segment "#{icon} #{signal}"
        else
          segment "#{icon}"
218
219
220
221
222
223
224
225
226
227
228
229
230
        end
      when /connecting/
        segment "#{icon} ..."
      else
        ''
      end
    else
      ''
    end
  end
end

class AudioStatus < StatusWidget
231
232
  def initialize
    super('audio')
233
234
235
236
237
238
239
240
241
242
243
244
245
246
  end

  private

  def start_monitor
    @p = IO.popen('pactl subscribe')
    @p.each do |line|
      if line =~ /\s(sink|server)\s/
        trigger
      end
    end
    @p.close
  end

247
  def cleanup
248
    Process.kill('TERM', @p.pid) if @p and !@p.closed?
249
250
251
    super
  end

252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
  def segments
    default_sink = `pactl info`.each_line do |line|
      if line =~ /^Default Sink:/
        break line.split(' ')[2]
      end
    end

    is_default = false
    description = nil; mute = nil; volume = nil; port = nil
    `pactl list sinks`.each_line do |line|
      tokens = line.split(' ')
      if line =~ /\tName:/
        is_default = tokens[1] == default_sink
      elsif is_default
        case line
        when /\tDescription: (.*)/
          description = $1
        when /\tMute:/
          mute = tokens[1]
        when /\tVolume:/
          volume = tokens[6]
        when /\tActive Port:/
          port = tokens[2]
          break
        end
      end
    end

280
    if description =~ /Built-in Audio (Analog|Stereo)/ or
281
282
       description == 'Dell AC511 USB SoundBar Analog Stereo' or
       description == 'DROP Panda Analog Stereo'
283
284
285
286
287
288
289
290
      icon = if mute == 'yes'
               fontawesome "\uf6a9"
             elsif port == 'analog-output-headphones'
               fontawesome "\uf025"
             else
               fontawesome "\uf028"
             end

291
      segment "#{icon} #{(volume.to_f * 2).round / 2.0}dB"
292
293
294
295
296
297
298
    else
      ''
    end
  end
end

class BacklightStatus < StatusWidget
299
300
  def initialize
    super('backlight')
301
302
303
304
305
  end

  private

  def start_monitor
306
307
308
309
310
311
312
    if Dir.glob('/sys/class/backlight/*/actual_brightness').empty?
      sleep
    else
      @p = IO.popen('inotifywait --quiet --monitor --event modify /sys/class/backlight/*/actual_brightness')
      @p.each { trigger }
      @p.close
    end
313
314
  end

315
316
317
318
319
  def cleanup
    Process.kill('TERM', @p.pid) if @p and !@p.closed?
    super
  end

320
321
322
  def segments
    brightness = `qdbus org.kde.Solid.PowerManagement /org/kde/Solid/PowerManagement/Actions/BrightnessControl brightness`.to_i
    max_brightness = `qdbus org.kde.Solid.PowerManagement /org/kde/Solid/PowerManagement/Actions/BrightnessControl brightnessMax`.to_i
323
324
325
326
327
328
    if max_brightness > 0
      percentage = brightness * 100.0 / max_brightness
      @clear_thread.exit if @clear_thread
      @clear_thread = Thread.new { sleep 2; puts '' }
      segment "#{fontawesome "\uf0eb"} #{percentage.round}%"
    end
329
330
331
332
  end
end

class ScreensaverStatus < StatusWidget
333
334
  def initialize(name = 'screensaver', timeout = 30)
    super(name, timeout)
335
336
337
338
339
340
341
342
343
344
  end

  private

  def start_monitor
    @p = IO.popen('inotifywait --quiet --monitor --event create --event delete --include screensaver.inhibit $XDG_RUNTIME_DIR')
    @p.each { trigger }
    @p.close
  end

345
  def cleanup
346
    Process.kill('TERM', @p.pid) if @p and !@p.closed?
347
348
349
    super
  end

350
351
  def segments
    if File.exist?(File.join(ENV['XDG_RUNTIME_DIR'], 'screensaver.inhibit'))
352
      if ENV['SWAYSOCK']
353
        system 'swaymsg \'[title="Sway Idle Inhibitor"] kill; exec kdialog --title "Sway Idle Inhibitor" --msgbox "This hidden box keeps Sway active"\''
354
355
356
      else
        system 'qdbus org.freedesktop.ScreenSaver /org/freedesktop/ScreenSaver org.freedesktop.ScreenSaver.SimulateUserActivity'
      end
357
358
      segment fontawesome "\uf108"
    else
359
      system 'swaymsg \'[title="Sway Idle Inhibitor"] kill\'' if ENV['SWAYSOCK']
360
361
362
363
364
      ''
    end
  end
end

James T. Lee's avatar
James T. Lee committed
365
class SwayidlerStatus < ScreensaverStatus
366
  def initialize
James T. Lee's avatar
James T. Lee committed
367
    super('swayidler', nil)
368
369
370
  end
end

371
class VpnStatus < StatusWidget
372
373
  def initialize
    super('vpn')
374
375
  end

376
377
378
  private

  def start_monitor
James T. Lee's avatar
James T. Lee committed
379
    @p = IO.popen('journalctl --user --follow --identifier=systemd')
380
381
382
383
384
    @p.each { trigger }
    @p.close
  end

  def cleanup
385
    Process.kill('TERM', @p.pid) if @p and !@p.closed?
386
387
388
389
    super
  end

  def segments
James T. Lee's avatar
James T. Lee committed
390
    if `systemctl --user is-active vpn`.chomp == 'active'
391
392
393
394
395
396
397
      segment fontawesome "\uf023"
    else
      ''
    end
  end
end

398
399
class SwayWorkspaces < StatusWidget
  def initialize(count)
400
    super('sway-workspaces')
401
    @count = count
402
    @fifos = (1..count).map { |num| mkfifo File.join(STATUS_FIFO_DIR, "sway-workspace-#{num}.fifo") }
403
404
  end

405
  def cleanup
406
    Process.kill('TERM', @p.pid) if @p and !@p.closed?
407
408
409
410
    @fifos.each_with_index do |fifo, i|
      fifo.close
      File.unlink File.join(STATUS_FIFO_DIR, "sway-workspace-#{i + 1}.fifo")
    end if @fifos
411
412
413
414
415
416
    super
  end

  private

  def start_monitor
417
418
419
420
421
422
    @p = IO.popen('swaymsg --monitor --type subscribe \'["workspace"]\'')
    @p.each do |line|
      if line =~ /^{/
        trigger
      end
    end
423
424
425
426
    @p.close
  end

  def segments
James T. Lee's avatar
James T. Lee committed
427
428
429
    workspaces = JSON.parse(`swaymsg -t get_workspaces`)
    workspaces.select! { |ws| ws['num'] > 0 }
    workspaces.sort! { |a, b| a['num'] <=> b['num'] }
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
    (1..@count).each do |num|
      fifo = @fifos[num - 1]
      workspace = workspaces.find { |ws| ws['num'] == num }
      if workspace
        if workspace['focused']
          fgcolor = '#282828'
          bgcolor = '#7cafc2'
          bold = true
        elsif workspace['urgent']
          fgcolor = '#282828'
          bgcolor = '#f7ca88'
          bold = false
        elsif workspace['visible']
          fgcolor = '#d8d8d8'
          bgcolor = '#383838'
          bold = false
        else
          fgcolor = '#d8d8d8'
          bgcolor = '#282828'
          bold = false
        end
        endsep = workspace == workspaces.last ? colorize(bgcolor, nil, '') : ''
        fifo.puts colorize(fgcolor, bgcolor, " #{workspace['name']} ", bold) + endsep
453
      else
454
        fifo.puts ''
455
      end
456
      fifo.flush
457
    end
458
    nil # don't output any segments
459
460
461
  end
end

462
# vim: filetype=ruby