diff --git a/config/astal/app.ts b/config/astal/app.ts index d711b7c..6a02a3f 100644 --- a/config/astal/app.ts +++ b/config/astal/app.ts @@ -1,14 +1,14 @@ import { App } from "astal/gtk4" import style from "./style.scss" -import notifications from "./components/notifications/handler"; +// import notifications from "./components/notifications/handler"; import Bar from "./components/bar/ui/Bar"; App.start({ instanceName: "runner", css: style, main() { - notifications.startNotificationHandler( App.get_monitors()[0] ); + // notifications.startNotificationHandler( App.get_monitors()[0] ); // TODO: Monitor handling Bar.Bar( App.get_monitors()[0] ); }, @@ -17,7 +17,8 @@ App.start({ // Notifications (TODO: Handle the arguments in the components themselves) if ( args[ 0 ] === 'notifier' ) { - res( notifications.cliHandler( args ) ); + res( 'Not available here yet, run astal -i notifier ' + args[ 1 ] ); + // res( notifications.cliHandler( args ) ); } else if ( args[ 0 ] === 'bar' ) { res( Bar.cliHandler( args ) ); } diff --git a/config/astal/components/bar/ui/Bar.tsx b/config/astal/components/bar/ui/Bar.tsx index 8970216..1f38694 100644 --- a/config/astal/components/bar/ui/Bar.tsx +++ b/config/astal/components/bar/ui/Bar.tsx @@ -3,40 +3,55 @@ import Hyprland from "./modules/Hyprland"; import Calendar from "./modules/Calendar"; import QuickView from "./modules/QuickView"; import SystemInfo from "./modules/SystemInfo"; +import { CenterBox } from "astal/gtk4/widget"; const Bar = (gdkmonitor: Gdk.Monitor) => { const { TOP, LEFT, RIGHT } = Astal.WindowAnchor; return ( - - - - - - - - - - - - - }> - + + + + + + } + centerWidget={} + endWidget={ + + + + + } + > + } + > ); -} +}; -const cliHandler = ( args: string[] ): string => { - return 'Not implemented'; -} +const cliHandler = (args: string[]): string => { + return "Not implemented"; +}; export default { Bar, - cliHandler + cliHandler, }; diff --git a/config/astal/components/bar/ui/QuickActions/QuickActions.tsx b/config/astal/components/bar/ui/QuickActions/QuickActions.tsx index 3bc681c..096af5f 100644 --- a/config/astal/components/bar/ui/QuickActions/QuickActions.tsx +++ b/config/astal/components/bar/ui/QuickActions/QuickActions.tsx @@ -1,6 +1,7 @@ import { Gtk } from "astal/gtk4" -import Network from "./modules/Networking/Network"; import Power from "./modules/Power"; +import Audio from "./modules/Audio/Audio"; +import Bluetooth from "./modules/Bluetooth/Bluetooth"; const QuickActions = () => { const popover = new Gtk.Popover( { cssClasses: [ 'quick-actions-popover' ] } ); @@ -12,9 +13,11 @@ const QuickActions = () => { const createQuickActionMenu = () => { - return + // TODO: For the future add WiFi / Networking back, for the time being remove, as unnecessary effort + return - + + } diff --git a/config/astal/components/bar/ui/QuickActions/modules/Audio/Audio.tsx b/config/astal/components/bar/ui/QuickActions/modules/Audio/Audio.tsx new file mode 100644 index 0000000..de5a95f --- /dev/null +++ b/config/astal/components/bar/ui/QuickActions/modules/Audio/Audio.tsx @@ -0,0 +1,178 @@ +import { bind, Binding } from "astal"; +import { Gtk } from "astal/gtk4"; +import AstalWp from "gi://AstalWp"; + +const wp = AstalWp.get_default()!; + +const AudioModule = () => { + const setVolumeSpeaker = (volume: number) => { + wp.defaultSpeaker.set_volume(volume / 100); + }; + + const setVolumeMicrophone = (volume: number) => { + wp.defaultMicrophone.set_volume(volume / 100); + }; + + const speakerSelector = SinkSelectPopover(AstalWp.MediaClass.AUDIO_SPEAKER); + const micSelector = SinkSelectPopover(AstalWp.MediaClass.AUDIO_MICROPHONE); + + return ( + + + + + 100 * v)} + max={100} + min={0} + step={1} + widthRequest={100} + onChangeValue={self => setVolumeSpeaker(self.value)} + > + + + + + + 100 * v, + )} + max={100} + min={0} + step={1} + widthRequest={100} + onChangeValue={self => setVolumeMicrophone(self.value)} + > + + + + ); +}; + +const SinkPicker = (type: AstalWp.MediaClass) => { + const devices = bind(wp, "endpoints"); + + return ( + + + + + {devices.as(d => { + return d.map(device => { + if (device.get_media_class() !== type) { + return ; + } + return ( + + ); + }); + })} + + + ); +}; + +const SinkSelectPopover = (type: AstalWp.MediaClass) => { + const popover = new Gtk.Popover(); + + popover.set_child(SinkPicker(type)); + + return popover; +}; + +export default { + AudioModule, +}; diff --git a/config/astal/components/bar/ui/QuickActions/modules/Bluetooth/Bluetooth.tsx b/config/astal/components/bar/ui/QuickActions/modules/Bluetooth/Bluetooth.tsx index e69de29..a87e522 100644 --- a/config/astal/components/bar/ui/QuickActions/modules/Bluetooth/Bluetooth.tsx +++ b/config/astal/components/bar/ui/QuickActions/modules/Bluetooth/Bluetooth.tsx @@ -0,0 +1,153 @@ +import { bind } from "astal"; +import { Gtk } from "astal/gtk4"; +import AstalBluetooth from "gi://AstalBluetooth"; +import BTDevice from "./Device"; + +const bt = AstalBluetooth.get_default(); + +const BluetoothModule = () => { + return ( + + + + + ); +}; + +const openBTPicker = () => { + picker.popup(); + try { + bt.adapter.start_discovery(); + } catch (_) {} +}; + +const BluetoothPickerList = () => { + return ( + bt.adapter.stop_discovery()}> + + + + {bind(bt, "devices").as(devices => { + return devices + .filter(device => { + if (device.get_connected()) { + return device; + } + }) + .map(device => { + return ; + }); + })} + + + + + + {bind(bt, "devices").as(devices => { + return devices + .filter(data => { + if (!data.get_connected()) { + return data; + } + }) + .map(device => { + return ; + }); + })} + + + + ); +}; + + + +const BluetoothPicker = () => { + const popover = new Gtk.Popover(); + + popover.set_child(BluetoothPickerList()); + popover.connect( 'closed', () => bt.adapter.stop_discovery() ); + + return popover; +}; + +const picker = BluetoothPicker(); + +export default { + BluetoothModule, +}; diff --git a/config/astal/components/bar/ui/QuickActions/modules/Bluetooth/Device.tsx b/config/astal/components/bar/ui/QuickActions/modules/Bluetooth/Device.tsx new file mode 100644 index 0000000..44b6256 --- /dev/null +++ b/config/astal/components/bar/ui/QuickActions/modules/Bluetooth/Device.tsx @@ -0,0 +1,52 @@ +import { bind } from "astal"; +import AstalBluetooth from "gi://AstalBluetooth"; + + +const BTDevice = ({ device }: { device: AstalBluetooth.Device }) => { + return ( + + ); +}; + + +export default BTDevice; diff --git a/config/astal/components/bar/ui/QuickActions/modules/Brightness/Brightness.tsx b/config/astal/components/bar/ui/QuickActions/modules/Brightness/Brightness.tsx new file mode 100644 index 0000000..e69de29 diff --git a/config/astal/components/bar/ui/QuickActions/modules/Networking/Network.tsx b/config/astal/components/bar/ui/QuickActions/modules/Networking/Network.tsx index 20e38ef..9c018dd 100644 --- a/config/astal/components/bar/ui/QuickActions/modules/Networking/Network.tsx +++ b/config/astal/components/bar/ui/QuickActions/modules/Networking/Network.tsx @@ -73,7 +73,8 @@ const Network = () => { } else { return 'Unavailable'; } - } )}> + } )} visible={bind(net.wifi, 'enabled').as( en => en )}> + } > - - - - - - } + return ( + + + + + + + + ); + }; - - popover.set_child( powerMenuBox() ); + popover.set_child(powerMenuBox()); return popover; -} +}; const Power = () => { const pm = PowerMenu(); - return - @@ -32,20 +35,18 @@ const NetworkWidget = () => { if ( state === AstalNetwork.State.CONNECTING ) { return 'chronometer-reset-symbolic'; } else if ( state === AstalNetwork.State.CONNECTED_LOCAL || state === AstalNetwork.State.CONNECTED_SITE || state === AstalNetwork.State.CONNECTED_GLOBAL ) { - print( 'Wired connected' ); return 'network-wired-activated-symbolic'; } else { - print( 'Unknown state' ); return 'paint-unknown-symbolic'; } - } )} cssClasses={[ 'network-widget' ]} visible={bind( network.wifi, 'state' ).as( state => state !== STATE.ACTIVATED )}> + } )} cssClasses={[ 'network-widget', 'quick-view-symbol' ]} visible={bind( network.wifi, 'state' ).as( state => state !== STATE.ACTIVATED )}> { if ( state === STATE.ACTIVATED ) { return network.wifi.iconName } else { return ''; } - } )} cssClasses={[ 'network-widget' ]} visible={bind( network.wifi, 'state' ).as( state => state === STATE.ACTIVATED )}> + } )} cssClasses={[ 'network-widget', 'quick-view-symbol' ]} visible={bind( network.wifi, 'state' ).as( state => state === STATE.ACTIVATED )}> @@ -53,15 +54,34 @@ const NetworkWidget = () => { const BluetoothWidget = () => { const bluetooth = AstalBluetooth.get_default(); - const enabled = bind( bluetooth, "isPowered" ); + const enabled = bind( bluetooth.adapter, "powered" ); const connected = bind( bluetooth, "isConnected" ); + + // For each connected BT device, render status + return + e )}> + c )}> + !c )}> + + !e )}> + + {bind( bluetooth, 'devices' ).as( devices => { + return devices.map( device => { + return c )}> + icon )}> + + + } ); + } )} + + } const BatteryWidget = () => { const battery = AstalBattery.get_default(); if ( battery.get_is_present() ) { - return + return icon )} cssClasses={[ 'quick-view-symbol' ]}> } else { return } @@ -70,31 +90,27 @@ const BatteryWidget = () => { const BrightnessWidget = () => { - // TODO: Finish (detect if there is a controllable screen) const brightness = Brightness.get_default(); const screen_brightness = bind( brightness, "screen" ); - return + return } const Audio = () => { const wireplumber = AstalWp.get_default(); if ( wireplumber ) { - const volume_speakers = bind( wireplumber.defaultSpeaker, 'volume' ); - return - - - - + icon )} cssClasses={[ 'quick-view-symbol' ]}> + icon )} cssClasses={[ 'quick-view-symbol' ]}> } else { print( '[ WirePlumber ] Could not connect, Audio support in bar will be missing' ); + return ; } - return null; } +// cssClasses={[ 'quick-view-symbol' ]} export default { QuickView diff --git a/config/astal/components/bar/ui/modules/SystemInfo.tsx b/config/astal/components/bar/ui/modules/SystemInfo.tsx index 53b4646..d909b2c 100644 --- a/config/astal/components/bar/ui/modules/SystemInfo.tsx +++ b/config/astal/components/bar/ui/modules/SystemInfo.tsx @@ -1,7 +1,46 @@ -import { exec, GLib } from "astal" +import { exec, GLib, interval, Variable } from "astal" +import { Gtk } from "astal/gtk4"; +import AstalBattery from "gi://AstalBattery?version=0.1"; + + +const FETCH_INTERVAL = 2000; + + +const cpuUtil = Variable( '0%' ); +const ramUtil = Variable( '0%' ); +const ramUsed = Variable( '0MiB' ); +const gpuUtil = Variable( '0%' ); +let gpuName = 'card1'; +let enabled = false; + +const refreshStats = (): Stats => { + gpuName = exec( `/bin/bash -c "ls /sys/class/drm/ | grep '^card[0-9]*$'"` ); + const cpuNameInSensors = 'CPUTIN' + const stats = { + kernel: exec( 'uname -sr' ), + netSpeed: exec( `/bin/bash -c "interface=$(ip route get 8.8.8.8 | awk '{print $5; exit}') && cat \"/sys/class/net/$interface/speed\""` ), + cpuTemp: exec( `/bin/bash -c "sensors | grep -m1 ${cpuNameInSensors} | awk '{print $2}'"` ), + cpuClk: exec( `awk '/cpu MHz/ {sum+=$4; ++n} END {print sum/n " MHz"}' /proc/cpuinfo` ), + gpuTemp: exec( `/bin/bash -c "sensors | grep -E 'edge' | awk '{print $2}'"` ), + gpuClk: exec( `/bin/bash -c "cat /sys/class/drm/${gpuName}/device/pp_dpm_sclk | grep '\\*' | awk '{print $2 $3}'"` ), + vram: Math.round( parseInt( exec( `cat /sys/class/drm/${gpuName}/device/mem_info_vram_used` ) ) / 1024 / 1024 ) + 'MiB', + availableVRAM: Math.round( parseInt( exec( `cat /sys/class/drm/${gpuName}/device/mem_info_vram_total` ) ) / 1024 / 1024 ) + 'MiB', + } + + return stats; +} + +const systemStats: Variable = Variable( refreshStats() ); + + +const availableFeatures = { + cpu: true, + ram: true, +} + const featureTest = () => { - // Check if awk is available + // Check if awk & sed are available try { exec( 'awk -V' ); exec( 'sed --version' ); @@ -11,44 +50,86 @@ const featureTest = () => { enabled = false; return; } + + // Check if mpstat is available try { - exec( 'mpstat' ); + exec( 'mpstat -V' ); } catch ( e ) { availableFeatures.cpu = false; printerr( '[ SysInfo ] Feature Test for CPU info failed. mpstat from the sysstat package missing!' ); } - - // Battery... acpi might be present, but potentially no bat } -let enabled = false; - -const availableFeatures = { - cpu: true, - ram: true, - bat: true, +const info = () => { + return + + + ; } + +const SystemInformationPanel = () => { + const popover = new Gtk.Popover(); + + popover.set_child( info() ); + + return popover; +} + + const sysInfoFetcher = () => { if ( enabled ) { if ( availableFeatures.cpu ) { - const cpuUtil = exec( 'mpstat | awk "/all/ {print(100 - $NF)"}' ); + cpuUtil.set( '' + Math.round( parseFloat( exec( `/bin/fish -c cpu-utilization` ) ) ) ); } if ( availableFeatures.ram ) { - const ramUtil = exec( `free | awk '/Mem/ { printf("%.2f\\n", ($3/$2)*100) }'` ); - } - if ( availableFeatures.bat ) { - const acpi = exec( `acpi -i | grep 'Battery'` ); - // TODO: Parse acpi output + ramUtil.set( '' + Math.round( parseFloat( exec( `/bin/bash -c "free | awk '/Mem/ { printf(\\"%.2f\\\\n\\", ($3/$2)*100) }'"` ) ) ) ); + ramUsed.set( exec( `/bin/bash -c \"free -h | awk '/^Mem:/ {print $3 \\" used of \\" $2}'\"` ).replaceAll( 'Gi', 'GiB' ).replaceAll( 'Mi', 'MiB' ) ); } + gpuUtil.set( exec( 'cat /sys/class/drm/card1/device/gpu_busy_percent' ) ); } } + +const panel = SystemInformationPanel(); + + const SystemInfo = () => { - return + featureTest(); + + const openSysInfo = async () => { + panel.popup(); + systemStats.set( refreshStats() ); + } + + if ( enabled ) { + sysInfoFetcher(); + interval( FETCH_INTERVAL, sysInfoFetcher ); + + return + } else { + return + } } export default { - SystemInfo + SystemInfo, + panel } diff --git a/config/astal/components/bar/ui/modules/stats.d.ts b/config/astal/components/bar/ui/modules/stats.d.ts new file mode 100644 index 0000000..ef6a04a --- /dev/null +++ b/config/astal/components/bar/ui/modules/stats.d.ts @@ -0,0 +1,10 @@ +interface Stats { + kernel: string; + netSpeed: string; + cpuTemp: string; + cpuClk: string; + gpuTemp: string; + gpuClk: string; + vram: string; + availableVRAM: string; +} diff --git a/config/astal/components/bar/util/brightness.ts b/config/astal/components/bar/util/brightness.ts index 6265966..d3e2706 100644 --- a/config/astal/components/bar/util/brightness.ts +++ b/config/astal/components/bar/util/brightness.ts @@ -12,7 +12,7 @@ export default class Brightness extends GObject.Object { static get_default() { if (!this.instance) this.instance = new Brightness() - + return this.instance } @@ -20,6 +20,10 @@ export default class Brightness extends GObject.Object { #kbd = get(`--device ${kbd} get`) #screenMax = get("max") #screen = get("get") / (get("max") || 1) + #screenAvailable = false + + @property(Boolean) + get screenAvailable() { return this.#screenAvailable } @property(Number) get kbd() { return this.#kbd } @@ -67,5 +71,12 @@ export default class Brightness extends GObject.Object { this.#kbd = Number(v) / this.#kbdMax this.notify("kbd") }) + + // Check if there is a screen available + try { + get( 'g -c backlight' ); + } catch ( _ ) { + this.#screenAvailable = false; + } } } diff --git a/config/astal/components/launcher/Launcher.tsx b/config/astal/components/launcher/Launcher.tsx new file mode 100644 index 0000000..e69de29 diff --git a/config/astal/components/notifications/handler.tsx b/config/astal/components/notifications/handler.tsx index 74972f0..8468ae0 100644 --- a/config/astal/components/notifications/handler.tsx +++ b/config/astal/components/notifications/handler.tsx @@ -91,7 +91,6 @@ const findNotificationByNotificationID = ( id: number ): number => { * @param id The notifd ID of the notification */ const addNotification = ( id: number ): void => { - print( '[ Notifications ] Notification with id ' + id + ' added.' ); const currIndex = Notifications.length; Notifications.push( { notifdID: id, diff --git a/config/astal/components/notifications/helper.ts b/config/astal/components/notifications/helper.ts new file mode 100644 index 0000000..78883f5 --- /dev/null +++ b/config/astal/components/notifications/helper.ts @@ -0,0 +1,12 @@ +import { Gtk, Gdk } from "astal/gtk4"; +import { GLib } from "astal"; + +export const isIcon = (icon: string) => { + const display = Gdk.Display.get_default(); + if (!display) return false; + const iconTheme = Gtk.IconTheme.get_for_display(display); + return iconTheme.has_icon(icon); +}; + +export const fileExists = (path: string) => + GLib.file_test(path, GLib.FileTest.EXISTS); diff --git a/config/astal/components/notifications/icon.tsx b/config/astal/components/notifications/icon.tsx new file mode 100644 index 0000000..8d6b75b --- /dev/null +++ b/config/astal/components/notifications/icon.tsx @@ -0,0 +1,24 @@ +import { Gtk } from "astal/gtk4"; +import Notifd from "gi://AstalNotifd"; +import { fileExists, isIcon } from "./helper"; + + +export function NotificationIcon(notification: Notifd.Notification) { + if ( notification.image || notification.appIcon || notification.desktopEntry) { + const icon = notification.image || notification.appIcon || notification.desktopEntry; + if (fileExists(icon)) { + return ( + + + + ); + } else if (isIcon(icon)) { + return ( + + + + ); + } + } + return null; +} diff --git a/config/astal/components/notifications/notifications.scss b/config/astal/components/notifications/notifications.scss index 3922722..ddc08de 100644 --- a/config/astal/components/notifications/notifications.scss +++ b/config/astal/components/notifications/notifications.scss @@ -13,7 +13,7 @@ window.NotificationHandler { all: unset; } -box.Notification { +box.notification { &:first-child { margin-top: 1rem; diff --git a/config/astal/components/notifications/notifications.tsx b/config/astal/components/notifications/notifications.tsx index 039374c..c63cb58 100644 --- a/config/astal/components/notifications/notifications.tsx +++ b/config/astal/components/notifications/notifications.tsx @@ -1,103 +1,113 @@ -// From astal examples +// From astal examples -import { GLib } from "astal" -import { Gtk } from "astal/gtk4" -import Notifd from "gi://AstalNotifd" +import { bind, GLib } from "astal"; +import { Gtk } from "astal/gtk4"; +import Notifd from "gi://AstalNotifd"; +import { NotificationIcon } from "./icon"; // import Pango from "gi://Pango?version=1.0" -const fileExists = (path: string) => - GLib.file_test(path, GLib.FileTest.EXISTS) +const fileExists = (path: string) => GLib.file_test(path, GLib.FileTest.EXISTS); -const time = (time: number, format = "%H:%M") => GLib.DateTime - .new_from_unix_local(time) - .format(format)! +const time = (time: number, format = "%H:%M") => + GLib.DateTime.new_from_unix_local(time).format(format)!; const urgency = (n: Notifd.Notification) => { - const { LOW, NORMAL, CRITICAL } = Notifd.Urgency + const { LOW, NORMAL, CRITICAL } = Notifd.Urgency; // match operator when? switch (n.urgency) { - case LOW: return "low" - case CRITICAL: return "critical" + case LOW: + return "low"; + case CRITICAL: + return "critical"; case NORMAL: - default: return "normal" + default: + return "normal"; } -} +}; type Props = { - delete: ( id: number ) => void; - notification: Notifd.Notification - id: number, -} + delete: (id: number) => void; + notification: Notifd.Notification; + id: number; +}; export default function Notification(props: Props) { - const { notification: n, id: id, delete: del } = props - const { START, CENTER, END } = Gtk.Align + const { notification: n, id: id, delete: del } = props; + const { START, CENTER, END } = Gtk.Align; - return - - {(n.appIcon || n.desktopEntry) ? : } - - - - {n.image && fileExists(n.image) ? - - - : } - {n.image ? - - - : } - + return ( + + + {n.appIcon || n.desktopEntry ? ( + + ) : ( + + )} - - {n.get_actions().length > 0 ? - {n.get_actions().map(({ label, id }) => ( - - ))} - : } - + halign={END} + label={time(n.time)} + /> + + + + + + {NotificationIcon(n)} + + + + + {n.get_actions().length > 0 ? ( + + {n.get_actions().map(({ label, id }) => ( + + ))} + + ) : ( + + )} + + ); } diff --git a/config/astal/style.scss b/config/astal/style.scss index 11a31b0..f474652 100644 --- a/config/astal/style.scss +++ b/config/astal/style.scss @@ -1,21 +1,12 @@ -// https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-24/gtk/theme/Adwaita/_colors-public.scss @use './components/notifications/notifications.scss'; -$fg-color: #{"@theme_fg_color"}; -$bg-color: #{"@theme_bg_color"}; +@use './components/bar/ui/bar.scss'; +@use './components/bar/ui/QuickActions/quickactions.scss'; -window.Bar { - background: transparent; - color: $fg-color; - font-weight: bold; - - >centerbox { - background: $bg-color; - border-radius: 10px; - margin: 8px; - } - - button { - border-radius: 8px; - margin: 2px; - } +* { + font-size: 1rem; +} + +empty { + min-width: 0; + background-color: transparent; } diff --git a/install b/install index 067f03d..5c8ce1c 100755 --- a/install +++ b/install @@ -11,7 +11,7 @@ read -p "Choose the configs to install, Laptop or Desktop (l/D): " platform yay -S hyprland hypridle hyprfreeze hyprlock plymouth aylurs-gtk-shell-git brightnessctl pulsemixer xdg-desktop-portal-hyprland # Audio, drivers, tools -yay -S pipewire pipewire-alsa pipewire-pulse pipewire-jack mesa fish thunar yazi wireplumber grimblast wl-clipboard wget vimiv zoxide trash-cli fzf ouch zathura +yay -S pipewire pipewire-alsa pipewire-pulse pipewire-jack mesa fish thunar yazi wireplumber grimblast wl-clipboard wget vimiv zoxide trash-cli fzf ouch zathura sensors radeontop lm-sensors # Set up yazi ya pack -a ndtoan96/ouch diff --git a/scripts/cpu-utilization b/scripts/cpu-utilization new file mode 100755 index 0000000..6fb3a8f --- /dev/null +++ b/scripts/cpu-utilization @@ -0,0 +1,20 @@ +#!/bin/bash + +# Get first snapshot +read cpu user nice system idle iowait irq softirq steal guest < /proc/stat +total1=$((user+nice+system+idle+iowait+irq+softirq+steal)) +idle1=$idle + +sleep 0.5 + +# Get second snapshot +read cpu user nice system idle iowait irq softirq steal guest < /proc/stat +total2=$((user+nice+system+idle+iowait+irq+softirq+steal)) +idle2=$idle + +# Calculate usage +total_diff=$((total2 - total1)) +idle_diff=$((idle2 - idle1)) +cpu_usage=$((100 * (total_diff - idle_diff) / total_diff)) + +echo "$cpu_usage"