summaryrefslogtreecommitdiff
path: root/cpu-widget/cpu-widget.lua
blob: f1056e9330df2b8fb47320ad2484d66d86c2d7ac (plain)
1
2
3
4
5
6
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
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
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
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
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
-------------------------------------------------
-- CPU Widget for Awesome Window Manager
-- Shows the current CPU utilization
-- More details could be found here:
-- https://github.com/streetturtle/awesome-wm-widgets/tree/master/cpu-widget

-- @author Pavel Makhov
-- @copyright 2020 Pavel Makhov
-------------------------------------------------

local awful = require("awful")
local watch = require("awful.widget.watch")
local wibox = require("wibox")
local beautiful = require("beautiful")
local gears = require("gears")

local CMD = [[sh -c "grep '^cpu.' /proc/stat; ps -eo 'pid:10,pcpu:5,pmem:5,comm:30,cmd' --sort=-pcpu ]]
    .. [[| grep -v [p]s | grep -v [g]rep | head -11 | tail -n +2"]]

-- A smaller command, less resource intensive, used when popup is not shown.
local CMD_slim = [[grep --max-count=1 '^cpu.' /proc/stat]]

local HOME_DIR = os.getenv("HOME")
local WIDGET_DIR = HOME_DIR .. '/.config/awesome/awesome-wm-widgets/cpu-widget'

local cpu_widget = {}
local cpu_rows = {
    spacing = 4,
    layout = wibox.layout.fixed.vertical,
}
local is_update = true
local process_rows = {
    layout = wibox.layout.fixed.vertical,
}

-- Remove spaces at end and beggining of a string
function trim(s)
   return (s:gsub("^%s*(.-)%s*$", "%1"))
end

-- Checks if a string starts with a another string
local function starts_with(str, start)
    return str:sub(1, #start) == start
end


local function create_textbox(args)
    return wibox.widget{
        text = args.text,
        align = args.align or 'left',
        markup = args.markup,
        forced_width = args.forced_width or 40,
        widget = wibox.widget.textbox
    }
end

local function create_process_header(params)
    local res = wibox.widget{
        create_textbox{markup = '<b>PID</b>'},
        create_textbox{markup = '<b>Name</b>'},
        {
            create_textbox{markup = '<b>%CPU</b>'},
            create_textbox{markup = '<b>%MEM</b>'},
            params.with_action_column and create_textbox{forced_width = 20} or nil,
            layout = wibox.layout.align.horizontal
        },
        layout  = wibox.layout.ratio.horizontal
    }
    res:ajust_ratio(2, 0.2, 0.47, 0.33)

    return res
end

local function create_kill_process_button()
    return wibox.widget{
        {
            id = "icon",
            image = WIDGET_DIR .. '/window-close-symbolic.svg',
            resize = false,
            opacity = 0.1,
            widget = wibox.widget.imagebox
        },
        widget = wibox.container.background
    }
end

local function worker(user_args)

    local args = user_args or {}

    local width = args.width or 50
    local step_width = args.step_width or 2
    local step_spacing = args.step_spacing or 1
    local color = args.color or beautiful.fg_normal
    local background_color = args.background_color or "#00000000"
    local enable_kill_button = args.enable_kill_button or false
    local process_info_max_length = args.process_info_max_length or -1
    local timeout = args.timeout or 1

    local cpugraph_widget = wibox.widget {
        max_value = 100,
        background_color = background_color,
        forced_width = width,
        step_width = step_width,
        step_spacing = step_spacing,
        widget = wibox.widget.graph,
        color = "linear:0,0:0,20:0,#FF0000:0.3,#FFFF00:0.6," .. color
    }

    -- This timer periodically executes the heavy command while the popup is open.
    -- It is stopped when the popup is closed and only the slim command is run then.
    -- This greatly improves performance while the popup is closed at the small cost
    -- of a slightly longer popup opening time.
    local popup_timer = gears.timer {
        timeout = timeout
    }

    local popup = awful.popup{
        ontop = true,
        visible = false,
        shape = gears.shape.rounded_rect,
        border_width = 1,
        border_color = beautiful.bg_normal,
        maximum_width = 300,
        offset = { y = 5 },
        widget = {}
    }

    -- Do not update process rows when mouse cursor is over the widget
    popup:connect_signal("mouse::enter", function() is_update = false end)
    popup:connect_signal("mouse::leave", function() is_update = true end)

    cpugraph_widget:buttons(
            awful.util.table.join(
                    awful.button({}, 1, function()
                        if popup.visible then
                            popup.visible = not popup.visible
                            -- When the popup is not visible, stop the timer
                            popup_timer:stop()
                        else
                            popup:move_next_to(mouse.current_widget_geometry)
                            -- Restart the timer, when the popup becomes visible
                            -- Emit the signal to start the timer directly and not wait the timeout first
                            popup_timer:start()
                            popup_timer:emit_signal("timeout")
                        end
                    end)
            )
    )

    --- By default graph widget goes from left to right, so we mirror it and push up a bit
    cpu_widget = wibox.widget {
        {
            cpugraph_widget,
            reflection = {horizontal = true},
            layout = wibox.container.mirror
        },
        bottom = 2,
        color = background_color,
        widget = wibox.container.margin
    }

    -- This part runs constantly, also when the popup is closed.
    -- It updates the graph widget in the bar.
    local maincpu = {}
    local reset_garbage_collector = 0
    watch(CMD_slim, timeout, function(widget, stdout)

        local _, user, nice, system, idle, iowait, irq, softirq, steal, _, _ =
            stdout:match('(%w+)%s+(%d+)%s(%d+)%s(%d+)%s(%d+)%s(%d+)%s(%d+)%s(%d+)%s(%d+)%s(%d+)%s(%d+)')

        local total = user + nice + system + idle + iowait + irq + softirq + steal

        local diff_idle = idle - tonumber(maincpu['idle_prev'] == nil and 0 or maincpu['idle_prev'])
        local diff_total = total - tonumber(maincpu['total_prev'] == nil and 0 or maincpu['total_prev'])
        local diff_usage = (1000 * (diff_total - diff_idle) / diff_total + 5) / 10

        maincpu['total_prev'] = total
        maincpu['idle_prev'] = idle

        widget:add_value(diff_usage)
        reset_garbage_collector = reset_garbage_collector + 1
        if (reset_garbage_collector > 5) then
            collectgarbage()
            reset_garbage_collector = 0
        end
    end,
    cpugraph_widget
    )

    -- This part runs whenever the timer is fired.
    -- It therefore only runs when the popup is open.
    local reset_garbage_collector = 0
    local cpus = {}
    popup_timer:connect_signal('timeout', function()
        awful.spawn.easy_async(CMD, function(stdout, _, _, _)
            local i = 1
            local j = 1
            for line in stdout:gmatch("[^\r\n]+") do
                if starts_with(line, 'cpu') then

                    if cpus[i] == nil then cpus[i] = {} end

                    local name, user, nice, system, idle, iowait, irq, softirq, steal, _, _ =
                        line:match('(%w+)%s+(%d+)%s(%d+)%s(%d+)%s(%d+)%s(%d+)%s(%d+)%s(%d+)%s(%d+)%s(%d+)%s(%d+)')

                    local total = user + nice + system + idle + iowait + irq + softirq + steal

                    local diff_idle = idle - tonumber(cpus[i]['idle_prev'] == nil and 0 or cpus[i]['idle_prev'])
                    local diff_total = total - tonumber(cpus[i]['total_prev'] == nil and 0 or cpus[i]['total_prev'])
                    local diff_usage = (1000 * (diff_total - diff_idle) / diff_total + 5) / 10

                    cpus[i]['total_prev'] = total
                    cpus[i]['idle_prev'] = idle

                    local row = wibox.widget
                    {
                        create_textbox{text = name},
                        create_textbox{text = math.floor(diff_usage) .. '%'},
                        {
                            max_value = 100,
                            value = diff_usage,
                            forced_height = 20,
                            forced_width = 150,
                            paddings = 1,
                            margins = 4,
                            border_width = 1,
                            border_color = beautiful.bg_focus,
                            background_color = beautiful.bg_normal,
                            bar_border_width = 1,
                            bar_border_color = beautiful.bg_focus,
                            color = "linear:150,0:0,0:0,#D08770:0.3,#BF616A:0.6," .. beautiful.fg_normal,
                            widget = wibox.widget.progressbar,

                        },
                        layout  = wibox.layout.ratio.horizontal
                    }
                    row:ajust_ratio(2, 0.15, 0.15, 0.7)
                    cpu_rows[i] = row
                    i = i + 1
                else
                    if is_update == true then

                        local pid = trim(string.sub(line, 1, 10))
                        local cpu = trim(string.sub(line, 12, 16))
                        local mem = trim(string.sub(line, 18, 22))
                        local comm = trim(string.sub(line, 24, 53))
                        local cmd = trim(string.sub(line, 54))

                        local kill_proccess_button = enable_kill_button and create_kill_process_button() or nil

                        local pid_name_rest = wibox.widget{
                            create_textbox{text = pid},
                            create_textbox{text = comm},
                            {
                                create_textbox{text = cpu, align = 'center'},
                                create_textbox{text = mem, align = 'center'},
                                kill_proccess_button,
                                layout = wibox.layout.fixed.horizontal
                            },
                            layout  = wibox.layout.ratio.horizontal
                        }
                        pid_name_rest:ajust_ratio(2, 0.2, 0.47, 0.33)

                        local row = wibox.widget {
                            {
                                pid_name_rest,
                                top = 4,
                                bottom = 4,
                                widget = wibox.container.margin
                            },
                            widget = wibox.container.background
                        }

                        row:connect_signal("mouse::enter", function(c) c:set_bg(beautiful.bg_focus) end)
                        row:connect_signal("mouse::leave", function(c) c:set_bg(beautiful.bg_normal) end)

                        if enable_kill_button then
                            row:connect_signal("mouse::enter", function() kill_proccess_button.icon.opacity = 1 end)
                            row:connect_signal("mouse::leave", function() kill_proccess_button.icon.opacity = 0.1 end)

                            kill_proccess_button:buttons(
                                awful.util.table.join( awful.button({}, 1, function()
                                    row:set_bg('#ff0000')
                                    awful.spawn.with_shell('kill -9 ' .. pid)
                                end) ) )
                        end

                        awful.tooltip {
                            objects = { row },
                            mode = 'outside',
                            preferred_positions = {'bottom'},
                            timer_function = function()
                                local text = cmd
                                if process_info_max_length > 0 and text:len() > process_info_max_length then
                                    text = text:sub(0, process_info_max_length - 3) .. '...'
                                end

                                return text
                                        :gsub('%s%-', '\n\t-') -- put arguments on a new line
                                        :gsub(':/', '\n\t\t:/') -- java classpath uses : to separate jars
                            end,
                        }

                        process_rows[j] = row

                        j = j + 1
                    end

                end
            end
            popup:setup {
                {
                    cpu_rows,
                    {
                        orientation = 'horizontal',
                        forced_height = 15,
                        color = beautiful.bg_focus,
                        widget = wibox.widget.separator
                    },
                    create_process_header{with_action_column = enable_kill_button},
                    process_rows,
                    layout = wibox.layout.fixed.vertical,
                },
                margins = 8,
                widget = wibox.container.margin
            }
            reset_garbage_collector = reset_garbage_collector + 1
            if (reset_garbage_collector > 10) then
                collectgarbage()
                reset_garbage_collector = 0
            end
        end)
    end)

    return cpu_widget
end

return setmetatable(cpu_widget, { __call = function(_, ...)
    return worker(...)
end })