Astal icon indicating copy to clipboard operation
Astal copied to clipboard

graphs

Open Astralchroma opened this issue 2 years ago • 10 comments

Astralchroma avatar Dec 10 '23 23:12 Astralchroma

graphs

No description provided.

Aylur avatar Dec 13 '23 15:12 Aylur

widget to display graphs not really sure what else to say heh

Astralchroma avatar Dec 13 '23 23:12 Astralchroma

Hi @Aylur, it would be great if some sort of graphs would be supported. My ideal end goal is to have something like qtile's CPUGraph, but of course, with arbitrary values.

I'd like to help out, however, my knowledge of JS/TS is basically non-existent. I can try to rip out relevant parts from something else that draws, such as CircularProgress, but I'm sure my attempt would require a lot of cleanup.

Thanks a lot!

otaj avatar Jan 13 '24 16:01 otaj

tbh I don't want to touch cairo

  1. because I just don't
  2. because I would have to port it to gtk4 eventually

Doing any graph widget like this is easy for anyone who knows cairo, JS/TS knowledge is not needed, since you can just copy the boilerplate from other subclasses and the only thing is implementing vfunc_draw

Since it would not make sense for graph widgets to have children it is just a matter of subclassing Gtk.DrawingArea

// this is how you can play with it in the config
import Variable from 'resource:///com/github/Aylur/ags/variable.js';
import Widget from 'resource:///com/github/Aylur/ags/widget.js';

const Graph = (value) => Widget.DrawingArea({
    setup(self) {
        self.hook(value, () => self.queue_draw())
        
        // you may have to set some size on it
        self.set_size_request(100, 100);

        self.connect('draw', (_, cr) => {
            // draw implementation
            // cr is the cairo.Context
        }),
    }
});

const value = Variable(0, { poll: [1000, Math.random] });
const graph = Graph(value);

If you feel like implementing this drawing function I can implement it as an ags subclass

Aylur avatar Jan 13 '24 19:01 Aylur

Alright, that sounds like a good starting point. I'll try to get to it by the next week.

otaj avatar Jan 13 '24 20:01 otaj

Welp, life happened, and here we are, a month later without anything from me. Sorry about that. If there is anyone who wants to take it up, feel free.

otaj avatar Feb 16 '24 12:02 otaj

hello, can I work on this

arkzuse avatar Mar 11 '24 21:03 arkzuse

In case anyone is interested, I built these two widgets for CPU and Memory (with the DrawingArea graph). Feel free to use/modify or ignore!

function Graph(value, history, color) {
    return Widget.DrawingArea({
        setup(self) {
            let data = [];
            self.hook(value, () => {
                data.push(value.value);
                if (data.length > history) {
                    data.shift();
                }
                self.queue_draw()
            });
            self.set_size_request(36, 12);

            self.connect('draw', (_, cr) => {
                let [width, height] = [self.get_allocated_width(), self.get_allocated_height()];
                cr.scale(width, height);
                cr.rectangle(0, 0, 1.0, 1.0);
                cr.fill();
                let samples = data.length - 1;
                let max = 100.0;
                cr.setSourceRGB(color[0], color[1], color[2]);
                if (samples > 0) {
                    cr.moveTo(1.0, 1.0);
                    let x = 1.0, y = 1.0 - data[samples] / max;
                    cr.lineTo(x, y);
                    for (let j = samples - 1; j >= 0; j--) {
                        y = 1.0 - data[j] / max;
                        x = j / samples;
                        cr.lineTo(x, y);
                    }
                    cr.lineTo(x, 1.0);
                    cr.closePath();
                    cr.fill();
                }
                cr.$dispose();
            });
        }
    })
}

let cpumonitor;

class CpuMonitor {
    constructor(interval, history, color) {
        this.cpu_load = Variable(0);
        this.prev_idle_time = 0;
        this.prev_total_time = 0;
        this.label = Widget.Label({
            label: this.cpu_load.bind().as(value => value.toFixed(0).toString() + "%"),
        });
        this.drawing = Graph(this.cpu_load, history, color);
        this.button = Widget.Button({
            child: this.drawing,
            on_clicked: () => Utils.execAsync("kitty btop --utf-force"),
            on_secondary_click: () => Utils.execAsync("kitty nvtop"),
            on_hover: () => {
                //top -d 0.2 -n 2 -b -o=-%CPU | tail -10 | tac | awk '{printf "%7.1f  %s\n", $9 ,$12}'
                this.button.tooltip_text = Utils.exec("bash -c \"top -d 0.2 -n 2 -b -o=-%CPU | tail -10 | tac | awk '{printf \\\"%7.1f %s\\n\\\", \$9, \$12}'\"");
            }
        });
        this.box = Widget.Box({
            class_name: "cpu-monitor",
            children: [ this.label, this.button ]
        });
        const id = Utils.interval(interval, () => {
            const stats = Utils.readFile("/proc/stat");
            const times = stats.split('\n', 1)[0].split(/\s+/);
            times.shift();
            const [
                user, nice, system, idle, iowait,
                irq, softirq, steal, guest, guest_nice,
            ] = times.map(s => {
                let v = parseInt(s);
                return v;
            });
            let total = user + nice + system + idle + iowait + irq + softirq + steal + guest + guest_nice;
            const idle_time_delta = idle - this.prev_idle_time;
            this.prev_idle_time = idle;
            const total_time_delta = total - this.prev_total_time;
            this.prev_total_time = total;
            const utilization =
                100.0 * (1.0 - idle_time_delta / total_time_delta);
            this.cpu_load.setValue(Math.round(utilization));
        }, this.box);
    }
    get_ui() {
        return this.box;
    }
}

function CpuGraph() {
    if (!cpumonitor) {
        cpumonitor = new CpuMonitor(2000, 8, [0.0, 0.57, 0.9]);
    }
    return cpumonitor.get_ui();
}

let memmonitor;

class MemMonitor {
    constructor(interval, history, color) {
        this.mem_load = Variable(0);
        this.mem_available = 0;
        this.mem_total = 0;
        this.label = Widget.Label({
            label: this.mem_load.bind().as(value => value.toFixed(0).toString() + "%"),
        });
        this.drawing = Graph(this.mem_load, history, color);
        this.button = Widget.Button({
            child: this.drawing,
            on_clicked: () => Utils.execAsync("kitty btop --utf-force"),
            on_secondary_click: () => Utils.execAsync("kitty nvtop"),
            on_hover: () => {
                let used = (this.mem_total - this.mem_available) / (1024 * 1024);
                let result = used.toFixed(1).toString() + " GB used\n\n";
                result += Utils.exec("bash -c \"ps -eo rss,comm --sort -rss --no-headers | head -n 10 | numfmt --to-unit=1024 --field 1 --padding 5\"");
                //this.button.tooltip_text = Utils.exec("bash -c \"top -d 0.2 -n 2 -b -o=-RES | tail -10 | tac | awk '{printf \\\"%8s %s\\n\\\", \$6, \$12}'\"");
                this.button.tooltip_text = result;
            }
        });
        this.box = Widget.Box({
            class_name: "mem-monitor",
            children: [ this.label, this.button ]
        });
        const id = Utils.interval(interval, () => {
            this.mem_available = 0;
            this.mem_total = 0;
            const stats = Utils.readFile("/proc/meminfo").split('\n');
            for (let stat of stats) {
                const memory = stat.split(/\s+/);
                if (memory[0] == "MemTotal:") {
                    this.mem_total = parseInt(memory[1]);
                } else if (memory[0] == "MemAvailable:") {
                    this.mem_available = parseInt(memory[1]);
                }
                if (this.mem_available > 0 && this.mem_total > 0)
                    break;
            }
            const utilization = (100.0 * (this.mem_total - this.mem_available)) / this.mem_total;
            this.mem_load.setValue(Math.round(utilization));
        }, this.box);
    }
    get_ui() {
        return this.box;
    }
}

function MemGraph() {
    if (!memmonitor) {
        memmonitor = new MemMonitor(5000, 8, [0.0, 0.7, 0.36]);
    }
    return memmonitor.get_ui();
}

dawsers avatar Apr 25 '24 08:04 dawsers

@dawsers would you mind giving an example how to activate your graphs in AGS's bar ?

Sorry if it's an obvious question, started using AGS only yesterday :D

ekkinox avatar Aug 26 '24 13:08 ekkinox

They are just regular Widgets you need to add to your Bar, and then your Bar to your App.

This is a part of my configuration where you can see the relevant parts. Remove the rest:

// layout of the bar
function Left() {
    return Widget.Box({
        spacing: 6,
        setup(self) {
            self.pack_start(Workspaces(), false, false, 0);
            self.pack_start(SubMap(), false, false, 0);
            self.pack_start(ClientTitle(), false, false, 0);
            self.pack_end(MediaPlayerBox(), false, false, 0);
        }
    })
}

function Right() {
    return Widget.Box({
        spacing: 6,
        setup(self) {
            self.pack_start(PackageUpdates(), false, false, 0);
            self.pack_start(IdleInhibitor(), false, false, 0);
            self.pack_start(KeyboardLayout(), false, false, 0);
            self.pack_end(SysTray(), false, false, 0);
            self.pack_end(Notifications(), false, false, 0);
            self.pack_end(MicrophoneIndicator(), false, false, 0);
            self.pack_end(SpeakerIndicator(), false, false, 0);
            self.pack_end(NetworkIndicator(), false, false, 0);
            self.pack_end(GpuGraph(), false, false, 0);
            self.pack_end(MemGraph(), false, false, 0);
            self.pack_end(CpuGraph(), false, false, 0);
            self.pack_end(Screenshot(), false, false, 0);
            self.pack_end(Weather(1200000), false, false, 0);
        }
    })
}
function Bar(monitor = 0) {
    if (monitor == 0) {
        return Widget.Window({
            name: `bar-${monitor}`, // name has to be unique
            class_name: "bar",
            monitor,
            anchor: ["top", "left", "right"],
            exclusivity: "exclusive",
            layer: "top",
            child: Widget.Box({
                setup(self) {
                    self.pack_start(Left(), true, true, 0);
                    self.pack_end(Right(), true, true, 0);
                    self.set_center_widget(Clock(monitor));
                },
            }),
        });
    } else {
        return Widget.Window({
            name: `bar-${monitor}`, // name has to be unique
            class_name: "bar",
            monitor,
            anchor: ["top", "left", "right"],
            exclusivity: "exclusive",
            layer: "top",
            child: Widget.CenterBox({
                start_widget: Workspaces(),
                center_widget: Clock(monitor),
            }),
        });
    }
}

App.config({
    style: "./style.css",
    windows: [
        NotificationPopups(),
        Bar(0),
        Bar(1)
    ],
})

And then add the relevant style.css

@define-color wb-green #00b35b;
@define-color wb-blue #0092e6;

.cpu-monitor {
    color: @wb-blue;
    margin-top: 6px;
    margin-bottom: 6px;
    padding-right: 0;
    /* avoid moving the rest when digit changes from 1 to anything else */
    min-width: 80px;
}

.mem-monitor {
    color: @wb-green;
    padding-left: 0;
    padding-right: 0px;
    margin-top: 6px;
    margin-bottom: 6px;
    min-width: 80px;
}

Of course you also need to have the dependencies installed in your system: btop etc.

dawsers avatar Aug 26 '24 20:08 dawsers