├── __pycache__ ├── actions.cpython-310.pyc ├── actions.cpython-312.pyc ├── app_info.cpython-310.pyc ├── app_info.cpython-312.pyc ├── clicks.cpython-310.pyc ├── clicks.cpython-312.pyc ├── config.cpython-310.pyc ├── config.cpython-312.pyc ├── date.cpython-312.pyc ├── effects.cpython-310.pyc ├── effects.cpython-312.pyc ├── items.cpython-310.pyc ├── items.cpython-312.pyc ├── media.cpython-310.pyc ├── media.cpython-312.pyc ├── raduis_image.cpython-310.pyc ├── raduis_image.cpython-312.pyc ├── round_image.cpython-310.pyc ├── round_image.cpython-312.pyc ├── sys_info.cpython-310.pyc ├── sys_info.cpython-312.pyc ├── timers.cpython-310.pyc ├── timers.cpython-312.pyc ├── updates.cpython-310.pyc ├── updates.cpython-312.pyc ├── widgets.cpython-310.pyc └── widgets.cpython-312.pyc ├── actions.py ├── app.py ├── app_images ├── app.png ├── clock.png ├── cpu.png ├── exit.png ├── folder.png ├── gpu.png ├── info.png ├── music.png ├── power.png ├── ram.png └── terminal.png ├── app_info.py ├── assistant.py ├── clicks.py ├── config.py ├── config ├── config.ini └── style.css ├── date.py ├── effects.py ├── items.py ├── media.py ├── raduis_image.py ├── readme.md ├── repo_images ├── 1.png ├── 2.png ├── 3.png ├── 4.png ├── 5.png └── capsule.png ├── sys_info.py ├── timers.py ├── updates.py └── widgets.py /__pycache__/actions.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NaturalCapsule/capsule/c654ad0e956a380d22ba39b1cfdffe949c6809f4/__pycache__/actions.cpython-310.pyc -------------------------------------------------------------------------------- /__pycache__/actions.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NaturalCapsule/capsule/c654ad0e956a380d22ba39b1cfdffe949c6809f4/__pycache__/actions.cpython-312.pyc -------------------------------------------------------------------------------- /__pycache__/app_info.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NaturalCapsule/capsule/c654ad0e956a380d22ba39b1cfdffe949c6809f4/__pycache__/app_info.cpython-310.pyc -------------------------------------------------------------------------------- /__pycache__/app_info.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NaturalCapsule/capsule/c654ad0e956a380d22ba39b1cfdffe949c6809f4/__pycache__/app_info.cpython-312.pyc -------------------------------------------------------------------------------- /__pycache__/clicks.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NaturalCapsule/capsule/c654ad0e956a380d22ba39b1cfdffe949c6809f4/__pycache__/clicks.cpython-310.pyc -------------------------------------------------------------------------------- /__pycache__/clicks.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NaturalCapsule/capsule/c654ad0e956a380d22ba39b1cfdffe949c6809f4/__pycache__/clicks.cpython-312.pyc -------------------------------------------------------------------------------- /__pycache__/config.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NaturalCapsule/capsule/c654ad0e956a380d22ba39b1cfdffe949c6809f4/__pycache__/config.cpython-310.pyc -------------------------------------------------------------------------------- /__pycache__/config.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NaturalCapsule/capsule/c654ad0e956a380d22ba39b1cfdffe949c6809f4/__pycache__/config.cpython-312.pyc -------------------------------------------------------------------------------- /__pycache__/date.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NaturalCapsule/capsule/c654ad0e956a380d22ba39b1cfdffe949c6809f4/__pycache__/date.cpython-312.pyc -------------------------------------------------------------------------------- /__pycache__/effects.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NaturalCapsule/capsule/c654ad0e956a380d22ba39b1cfdffe949c6809f4/__pycache__/effects.cpython-310.pyc -------------------------------------------------------------------------------- /__pycache__/effects.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NaturalCapsule/capsule/c654ad0e956a380d22ba39b1cfdffe949c6809f4/__pycache__/effects.cpython-312.pyc -------------------------------------------------------------------------------- /__pycache__/items.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NaturalCapsule/capsule/c654ad0e956a380d22ba39b1cfdffe949c6809f4/__pycache__/items.cpython-310.pyc -------------------------------------------------------------------------------- /__pycache__/items.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NaturalCapsule/capsule/c654ad0e956a380d22ba39b1cfdffe949c6809f4/__pycache__/items.cpython-312.pyc -------------------------------------------------------------------------------- /__pycache__/media.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NaturalCapsule/capsule/c654ad0e956a380d22ba39b1cfdffe949c6809f4/__pycache__/media.cpython-310.pyc -------------------------------------------------------------------------------- /__pycache__/media.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NaturalCapsule/capsule/c654ad0e956a380d22ba39b1cfdffe949c6809f4/__pycache__/media.cpython-312.pyc -------------------------------------------------------------------------------- /__pycache__/raduis_image.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NaturalCapsule/capsule/c654ad0e956a380d22ba39b1cfdffe949c6809f4/__pycache__/raduis_image.cpython-310.pyc -------------------------------------------------------------------------------- /__pycache__/raduis_image.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NaturalCapsule/capsule/c654ad0e956a380d22ba39b1cfdffe949c6809f4/__pycache__/raduis_image.cpython-312.pyc -------------------------------------------------------------------------------- /__pycache__/round_image.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NaturalCapsule/capsule/c654ad0e956a380d22ba39b1cfdffe949c6809f4/__pycache__/round_image.cpython-310.pyc -------------------------------------------------------------------------------- /__pycache__/round_image.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NaturalCapsule/capsule/c654ad0e956a380d22ba39b1cfdffe949c6809f4/__pycache__/round_image.cpython-312.pyc -------------------------------------------------------------------------------- /__pycache__/sys_info.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NaturalCapsule/capsule/c654ad0e956a380d22ba39b1cfdffe949c6809f4/__pycache__/sys_info.cpython-310.pyc -------------------------------------------------------------------------------- /__pycache__/sys_info.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NaturalCapsule/capsule/c654ad0e956a380d22ba39b1cfdffe949c6809f4/__pycache__/sys_info.cpython-312.pyc -------------------------------------------------------------------------------- /__pycache__/timers.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NaturalCapsule/capsule/c654ad0e956a380d22ba39b1cfdffe949c6809f4/__pycache__/timers.cpython-310.pyc -------------------------------------------------------------------------------- /__pycache__/timers.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NaturalCapsule/capsule/c654ad0e956a380d22ba39b1cfdffe949c6809f4/__pycache__/timers.cpython-312.pyc -------------------------------------------------------------------------------- /__pycache__/updates.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NaturalCapsule/capsule/c654ad0e956a380d22ba39b1cfdffe949c6809f4/__pycache__/updates.cpython-310.pyc -------------------------------------------------------------------------------- /__pycache__/updates.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NaturalCapsule/capsule/c654ad0e956a380d22ba39b1cfdffe949c6809f4/__pycache__/updates.cpython-312.pyc -------------------------------------------------------------------------------- /__pycache__/widgets.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NaturalCapsule/capsule/c654ad0e956a380d22ba39b1cfdffe949c6809f4/__pycache__/widgets.cpython-310.pyc -------------------------------------------------------------------------------- /__pycache__/widgets.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NaturalCapsule/capsule/c654ad0e956a380d22ba39b1cfdffe949c6809f4/__pycache__/widgets.cpython-312.pyc -------------------------------------------------------------------------------- /actions.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import os 3 | import shlex 4 | 5 | def launch_app(widget, exec): 6 | if exec == 'kitty' or exec =='htop' or exec == 'alacritty' or exec == 'yazi' or exec == 'vim' or exec == 'nvim': 7 | subprocess.Popen(["kitty", "-e", "sh", "-c", exec]) 8 | else: 9 | cmd = shlex.split(exec) 10 | subprocess.Popen(cmd, start_new_session = True, cwd = os.path.expanduser("~")) 11 | 12 | def open_terminal(widget, term): 13 | if term is not None or term != '' or len(term) != 0: 14 | terminal = shlex.split(term) 15 | subprocess.Popen(terminal, shell = True, cwd=os.path.expanduser("~")) 16 | else: 17 | try: 18 | subprocess.Popen( 19 | ["kitty"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell = True, cwd=os.path.expanduser("~") 20 | ) 21 | 22 | except FileNotFoundError: 23 | try: 24 | subprocess.Popen(["alacritty"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell = True, cwd=os.path.expanduser("~")) 25 | except FileNotFoundError: 26 | print("Tried Kitty and Alacritty and None of them opened, make sure its installed on your system!.") 27 | 28 | def exit_(widget): 29 | subprocess.Popen( 30 | ["exit"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell = True, cwd=os.path.expanduser("~") 31 | ) 32 | 33 | def open_fileM(widget, fileM): 34 | if fileM is not None or fileM != '' or len(fileM) != 0: 35 | file_manager = shlex.split(fileM) 36 | subprocess.Popen(file_manager, shell = True, cwd=os.path.expanduser("~")) 37 | 38 | else: 39 | try: 40 | subprocess.Popen( 41 | ["pcmanfm"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell = True, cwd=os.path.expanduser("~") 42 | ) 43 | except FileNotFoundError: 44 | try: 45 | subprocess.Popen(["thunar"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell = True, cwd=os.path.expanduser("~")) 46 | except FileNotFoundError: 47 | try: 48 | subprocess.Popen(["dolphin"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell = True, cwd=os.path.expanduser("~")) 49 | except FileNotFoundError: 50 | print("Tried pcmanfm, thunar and dolphin, None of them worked, make sure its installed on your system") 51 | 52 | def previous_track_func(widget): 53 | os.system("playerctl previous") 54 | 55 | def next_track_func(widget): 56 | os.system("playerctl next") 57 | 58 | def reset_func(widget): 59 | os.system('playerctl position 0') 60 | 61 | def pause_play_func(widget): 62 | os.system('playerctl play-pause') 63 | 64 | def fast_forward_func(widget): 65 | os.system("playerctl position 10+") 66 | 67 | def backward_func(widget): 68 | os.system("playerctl position 10-") 69 | 70 | def shutdown_machine(widget): 71 | subprocess.Popen(['shutdown', 'now']) 72 | 73 | def reboot_machine(widget): 74 | subprocess.Popen(['reboot']) 75 | 76 | def lock_machine(widget): 77 | subprocess.Popen(['hyprlock']) 78 | 79 | def hib_machine(widget): 80 | subprocess.Popen(['systemctl', 'hibernate']) 81 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import gi 2 | import config 3 | import widgets 4 | gi.require_version('Gtk', '3.0') 5 | gi.require_version('Gdk', '3.0') 6 | 7 | from gi.repository import Gtk, Gdk 8 | from timers import RunTimers, update_media_, update_time_, update_date_ 9 | from media import MediaPlayerMonitor 10 | 11 | from actions import * 12 | from sys_info import * 13 | from items import * 14 | from clicks import * 15 | from effects import * 16 | 17 | 18 | from app_info import get_app_info 19 | 20 | 21 | class Capsule(Gtk.Window): 22 | def __init__(self): 23 | super().__init__(type=Gtk.WindowType.POPUP) 24 | self.config = config 25 | self.apps_ = {} 26 | 27 | 28 | self.set_position(Gtk.WindowPosition.MOUSE) 29 | self.set_skip_taskbar_hint(True) 30 | self.set_skip_pager_hint(True) 31 | self.set_keep_above(True) 32 | self.set_decorated(False) 33 | self.set_resizable(False) 34 | if self.config.use_blur: 35 | self.set_app_paintable(True) 36 | self.get_style_context().add_class("window") 37 | 38 | screen = self.get_screen() 39 | visual = screen.get_rgba_visual() 40 | if visual and screen.is_composited(): 41 | self.set_visual(visual) 42 | 43 | self.main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) 44 | self.add(self.main_box) 45 | 46 | self.open_submenus = {} 47 | self.menu_items = [] 48 | self.current_selected_index = 0 49 | self.active_submenu = None 50 | 51 | 52 | self.build_menu() 53 | self.connect("key-press-event", self.on_key_press) 54 | 55 | RunTimers(widgets.cpu_temp, widgets.cpu_usage, widgets.ram_usage, widgets.used_ram, 56 | widgets.gpu_temp, widgets.gpu_usage, widgets.gpu_vram, widgets.gpu_speed, 57 | widgets.gpu_power) 58 | 59 | 60 | 61 | update_media_(widgets.title_label, widgets.media_image) 62 | update_time_(widgets.time_label) 63 | # update_date_(widgets.rounded_calendar) 64 | 65 | 66 | self.show_all() 67 | 68 | def on_focus_out(self, widget, event): 69 | self.destroy() 70 | Gtk.main_quit() 71 | return False 72 | 73 | def on_key_press_(self, widget, event): 74 | key = Gdk.keyval_name(event.keyval) 75 | 76 | # children = self.listbox.get_children() 77 | # if not children: 78 | # return 79 | 80 | # current_row = self.listbox.get_focus_child() 81 | # if not current_row: 82 | # current_row = children[0] 83 | 84 | # try: 85 | # current_index = children.index(current_row) 86 | # except ValueError: 87 | # current_index = 0 88 | 89 | if key == "Return": 90 | selected_row = widgets.listbox.get_selected_row() 91 | if selected_row: 92 | self.run_selected_program(selected_row) 93 | return True 94 | 95 | # if key == "j": 96 | # new_index = (current_index - 1) % len(children) 97 | # elif key == "k": 98 | # new_index = (current_index + 1) % len(children) 99 | # else: 100 | # return 101 | 102 | # self.listbox.select_row(children[new_index]) 103 | # children[new_index].grab_focus() 104 | 105 | def run_selected_program(self, row): 106 | event_box = row.get_child() 107 | box = event_box.get_child() 108 | 109 | try: 110 | label = box.get_children()[1] 111 | app_name = label.get_text() 112 | except IndexError: 113 | label = box.get_children()[0] 114 | app_name = label.get_text() 115 | 116 | print(f"Launching {app_name}...") 117 | launch_app(widget = None, exec=self.apps_.get(app_name)) 118 | exit(0) 119 | 120 | 121 | def on_key_press(self, widget, event): 122 | if event.keyval == Gdk.KEY_Escape: 123 | if self.open_submenus: 124 | for submenu_id in list(self.open_submenus.keys()): 125 | self.open_submenus[submenu_id].destroy() 126 | try: 127 | del self.open_submenus[submenu_id] 128 | except KeyError: 129 | pass 130 | self.active_submenu = None 131 | return True 132 | else: 133 | self.destroy() 134 | Gtk.main_quit() 135 | return True 136 | elif event.keyval == Gdk.KEY_Up: 137 | if self.active_submenu and hasattr(self.active_submenu, 'navigate'): 138 | self.active_submenu.navigate('up') 139 | else: 140 | if self.current_selected_index > 0: 141 | self.select_menu_item(self.current_selected_index - 1) 142 | return True 143 | elif event.keyval == Gdk.KEY_Down: 144 | if self.active_submenu and hasattr(self.active_submenu, 'navigate'): 145 | self.active_submenu.navigate('down') 146 | else: 147 | if self.current_selected_index < len(self.menu_items) - 1: 148 | self.select_menu_item(self.current_selected_index + 1) 149 | return True 150 | elif event.keyval == Gdk.KEY_Left: 151 | if self.active_submenu: 152 | for submenu_id in list(self.open_submenus.keys()): 153 | self.open_submenus[submenu_id].destroy() 154 | try: 155 | del self.open_submenus[submenu_id] 156 | except KeyError: 157 | pass 158 | self.active_submenu = None 159 | return True 160 | elif event.keyval == Gdk.KEY_Right: 161 | if not self.active_submenu and self.current_selected_index < len(self.menu_items): 162 | item = self.menu_items[self.current_selected_index] 163 | if hasattr(item, 'submenu_func') and item.submenu_func: 164 | menu_id = item.menu_id 165 | self.toggle_submenu(item.submenu_func, item, menu_id) 166 | return True 167 | elif event.keyval == Gdk.KEY_Return: 168 | if self.active_submenu and hasattr(self.active_submenu, 'activate_selected'): 169 | self.active_submenu.activate_selected() 170 | elif self.current_selected_index < len(self.menu_items): 171 | item = self.menu_items[self.current_selected_index] 172 | if hasattr(item, 'click_handler'): 173 | item.click_handler(item, None) 174 | elif hasattr(item, 'submenu_func') and item.submenu_func: 175 | menu_id = item.menu_id 176 | self.toggle_submenu(item.submenu_func, item, menu_id) 177 | return True 178 | return False 179 | 180 | def select_menu_item(self, index): 181 | if 0 <= self.current_selected_index < len(self.menu_items): 182 | self.menu_items[self.current_selected_index].get_style_context().remove_class("menu-item-hover") 183 | self.menu_items[self.current_selected_index].unset_state_flags(Gtk.StateFlags.PRELIGHT) 184 | self.current_selected_index = index 185 | if 0 <= index < len(self.menu_items): 186 | self.menu_items[index].get_style_context().add_class("menu-item-hover") 187 | self.menu_items[index].set_state_flags(Gtk.StateFlags.PRELIGHT, True) 188 | 189 | def on_row_selected(self, listbox, row): 190 | for child in listbox.get_children(): 191 | child.get_style_context().remove_class("App-List-selected-row") 192 | 193 | if row: 194 | row.get_style_context().add_class("App-List-selected-row") 195 | 196 | def on_search_changed(self, search_entry): 197 | widgets.listbox.invalidate_filter() 198 | 199 | def filter_func(self, row, data): 200 | 201 | if not row: 202 | return False 203 | 204 | event_box = row.get_child() 205 | if not event_box: 206 | return False 207 | 208 | hbox = event_box.get_child() 209 | if not hbox: 210 | return False 211 | 212 | label = None 213 | for child in hbox.get_children(): 214 | if isinstance(child, Gtk.Label): 215 | label = child 216 | break 217 | 218 | if not label: 219 | return False 220 | 221 | query = widgets.search_entry.get_text().lower() 222 | return query in label.get_text().lower() 223 | 224 | def build_menu(self): 225 | self.media_box = create_menu_item("Media Control", "app_images/music.png", self.on_hover_enter, on_hover_leave) 226 | self.terminal_box = create_menu_item("Terminal", "app_images/terminal.png", self.on_hover_enter, on_hover_leave) 227 | self.exit_box = create_menu_item("Exit", "app_images/exit.png", self.on_hover_enter, on_hover_leave) 228 | self.file_manager_box = create_menu_item("Open File Manager", "app_images/folder.png", self.on_hover_enter, on_hover_leave) 229 | self.app_box = create_menu_item("Applications", "app_images/app.png", self.on_hover_enter, on_hover_leave) 230 | self.power_box = create_menu_item("Power Settings", "app_images/power.png", self.on_hover_enter, on_hover_leave) 231 | self.system_box = create_menu_item("Hardware Info", "app_images/info.png", self.on_hover_enter, on_hover_leave) 232 | self.timer_box = create_menu_item('Time & Date', 'app_images/clock.png', self.on_hover_enter, on_hover_leave) 233 | 234 | self.media_submenu = self.build_media_control_menu() 235 | self.media_box.submenu_func = self.media_submenu 236 | self.media_box.menu_id = "media" 237 | 238 | self.terminal_box.click_handler = lambda w, e, d=self.destroy, t=self.config.terminal: on_terminal_click(w, e, d, t) 239 | self.terminal_box.connect("button-press-event", lambda w, e, d = self.destroy, t = self.config.terminal: on_terminal_click(w, e, d, t)) 240 | 241 | self.exit_box.click_handler = lambda w, e, a=self.destroy: on_exit_click(w, e, a) 242 | self.exit_box.connect("button-press-event", lambda w, e, a=self.destroy: on_exit_click(w, e, a)) 243 | 244 | self.file_manager_box.click_handler = lambda w, e, d=self.destroy, m=self.config.file_manager: on_fileM_click(w, e, d, m) 245 | self.file_manager_box.connect("button-press-event", lambda w, e, d=self.destroy, m=self.config.file_manager: on_fileM_click(w, e, d, m)) 246 | 247 | self.app_submenu = self.build_application_menu() 248 | self.app_box.submenu_func = self.app_submenu 249 | self.app_box.menu_id = "app" 250 | 251 | self.power_submenu = self.build_power_menu() 252 | self.power_box.submenu_func = self.power_submenu 253 | self.power_box.menu_id = "power" 254 | 255 | self.system_submenu = self.build_system_info_menu() 256 | self.system_box.submenu_func = self.system_submenu 257 | self.system_box.menu_id = "system" 258 | 259 | self.timer_submenu = self.build_time_menu() 260 | self.timer_box.submenu_func = self.timer_submenu 261 | self.timer_box.menu_id = "time" 262 | 263 | self.media_box.connect("button-press-event", lambda w, e: self.toggle_submenu(self.media_submenu, w, "media")) 264 | self.app_box.connect("button-press-event", lambda w, e: self.toggle_submenu(self.app_submenu, w, "app")) 265 | self.power_box.connect("button-press-event", lambda w, e: self.toggle_submenu(self.power_submenu, w, "power")) 266 | self.system_box.connect("button-press-event", lambda w, e: self.toggle_submenu(self.system_submenu, w, "system")) 267 | self.timer_box.connect("button-press-event", lambda w, e: self.toggle_submenu(self.timer_submenu, w, "time")) 268 | 269 | self.menu_items = [ 270 | self.media_box, 271 | self.app_box, 272 | self.system_box, 273 | self.power_box, 274 | self.timer_box, 275 | self.terminal_box, 276 | self.file_manager_box, 277 | self.exit_box 278 | ] 279 | 280 | self.main_box.pack_start(self.media_box, False, False, 0) 281 | self.main_box.pack_start(self.app_box, False, False, 0) 282 | self.main_box.pack_start(self.system_box, False, False, 0) 283 | self.main_box.pack_start(self.power_box, False, False, 0) 284 | self.main_box.pack_start(self.timer_box, False, False, 0) 285 | 286 | self.main_box.pack_start(widgets.separator, False, False, 4) 287 | 288 | self.main_box.pack_start(self.terminal_box, False, False, 0) 289 | self.main_box.pack_start(self.file_manager_box, False, False, 0) 290 | self.main_box.pack_start(self.exit_box, False, False, 0) 291 | 292 | 293 | def on_hover_enter(self, widget, event): 294 | widget.get_style_context().add_class("menu-item-hover") 295 | widget.set_state_flags(Gtk.StateFlags.PRELIGHT, True) 296 | if widget in self.menu_items: 297 | self.current_selected_index = self.menu_items.index(widget) 298 | return False 299 | 300 | 301 | def toggle_submenu(self, submenu_func, parent_widget, menu_id): 302 | if menu_id in self.open_submenus and self.open_submenus[menu_id].get_visible(): 303 | self.open_submenus[menu_id].destroy() 304 | try: 305 | del self.open_submenus[menu_id] 306 | except KeyError: 307 | pass 308 | self.active_submenu = None 309 | else: 310 | for id_key in list(self.open_submenus.keys()): 311 | if self.open_submenus[id_key] and self.open_submenus[id_key].get_visible(): 312 | self.open_submenus[id_key].destroy() 313 | try: 314 | del self.open_submenus[id_key] 315 | except KeyError: 316 | pass 317 | 318 | self.show_submenu(submenu_func, parent_widget, menu_id) 319 | 320 | def show_submenu(self, submenu_func, parent_widget, menu_id): 321 | window_x, window_y = self.get_position() 322 | parent_alloc = parent_widget.get_allocation() 323 | 324 | x = window_x + parent_alloc.width 325 | y = window_y + parent_alloc.y 326 | 327 | submenu = submenu_func() 328 | submenu.move(x, y) 329 | 330 | self.open_submenus[menu_id] = submenu 331 | self.active_submenu = submenu 332 | 333 | submenu.connect("destroy", lambda w: self.on_submenu_destroyed(menu_id)) 334 | 335 | submenu.connect("key-press-event", self.on_key_press) 336 | 337 | submenu.show_all() 338 | 339 | def on_submenu_destroyed(self, menu_id): 340 | if menu_id in self.open_submenus: 341 | del self.open_submenus[menu_id] 342 | self.active_submenu = None 343 | 344 | def build_media_control_menu(self): 345 | def create_submenu(): 346 | window, vbox = build_submenu_window("Media Control") 347 | 348 | text = '▶' 349 | media = MediaPlayerMonitor() 350 | if media.playback_status == 'Paused': 351 | text = '▶' 352 | elif media.playback_status == 'Playing': 353 | text = '❚❚' 354 | 355 | if text == '❚❚': 356 | reset = ' ↺' 357 | else: 358 | reset = '↺' 359 | 360 | items = [ 361 | ("⏮", None, backward_func), 362 | (text, None, pause_play_func), 363 | ("⏭", None, fast_forward_func), 364 | ("<<", None, previous_track_func), 365 | (reset, None, reset_func), 366 | (">>", None, next_track_func), 367 | ] 368 | 369 | widgets.title_label.set_margin_start(20) 370 | widgets.title_label.set_margin_end(20) 371 | widgets.title_label.set_xalign(0) 372 | 373 | grid = Gtk.Grid() 374 | 375 | hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) 376 | vbox.pack_start(hbox, False, False, 0) 377 | 378 | hbox.pack_start(widgets.media_image, False, False, 10) 379 | hbox.pack_start(widgets.title_label, False, False, 0) 380 | vbox.pack_start(widgets.media_separator, False, False, 0) 381 | 382 | window.submenu_items = [] 383 | 384 | window.current_selected_index = 0 385 | 386 | column = 0 387 | row = 0 388 | count = 0 389 | 390 | vbox.pack_start(grid, False, False, 0) 391 | 392 | for label_text, icon_path, action_func in items: 393 | item = create_submenu_item(label_text, icon_path, on_submenu_hover_enter=lambda w, e, a=self.active_submenu: on_submenu_hover_enter(w, e, a), on_submenu_hover_leave=on_submenu_hover_leave) 394 | item.action_func = action_func 395 | item.connect("button-press-event", lambda w, e, f=action_func: on_submenu_item_click(w, e, f)) 396 | if count == 3: 397 | column += 1 398 | row = 0 399 | grid.attach(item, row, column, 1, 1) 400 | row += 1 401 | count += 1 402 | window.submenu_items.append(item) 403 | 404 | def navigate(direction): 405 | if direction == 'up' and window.current_selected_index > 0: 406 | select_item(window.current_selected_index - 1) 407 | elif direction == 'down' and window.current_selected_index < len(window.submenu_items) - 1: 408 | select_item(window.current_selected_index + 1) 409 | 410 | def select_item(index): 411 | if 0 <= window.current_selected_index < len(window.submenu_items): 412 | window.submenu_items[window.current_selected_index].get_style_context().remove_class("menu-item-hover") 413 | window.submenu_items[window.current_selected_index].unset_state_flags(Gtk.StateFlags.PRELIGHT) 414 | 415 | window.current_selected_index = index 416 | if 0 <= index < len(window.submenu_items): 417 | window.submenu_items[index].get_style_context().add_class("menu-item-hover") 418 | window.submenu_items[index].set_state_flags(Gtk.StateFlags.PRELIGHT, True) 419 | 420 | def activate_selected(): 421 | if 0 <= window.current_selected_index < len(window.submenu_items): 422 | item = window.submenu_items[window.current_selected_index] 423 | if hasattr(item, 'action_func'): 424 | item.action_func(item) 425 | window.destroy() 426 | for w in Gtk.Window.list_toplevels(): 427 | w.destroy() 428 | Gtk.main_quit() 429 | 430 | window.navigate = navigate 431 | window.activate_selected = activate_selected 432 | 433 | return window 434 | 435 | return create_submenu 436 | 437 | 438 | def build_application_menu(self): 439 | def create_submenu(): 440 | window, vbox = build_submenu_window("Applications") 441 | 442 | widgets.listbox.connect("key-press-event", self.on_key_press_) 443 | widgets.search_entry.connect("changed", self.on_search_changed) 444 | widgets.search_entry.set_icon_from_icon_name(Gtk.EntryIconPosition.PRIMARY, "view-app-grid-symbolic") 445 | 446 | widgets.listbox.set_filter_func(self.filter_func, None) 447 | widgets.listbox.connect("row-selected", self.on_row_selected) 448 | 449 | scrolled_window = Gtk.ScrolledWindow() 450 | scrolled_window.get_style_context().add_class("Scroll-Window") 451 | scrolled_window.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) 452 | scrolled_window.set_min_content_height(300) 453 | scrolled_window.set_max_content_height(500) 454 | scrolled_window.set_propagate_natural_height(True) 455 | 456 | 457 | window.submenu_items = [] 458 | window.current_selected_index = 0 459 | 460 | 461 | scrolled_window.add(widgets.listbox) 462 | vbox.pack_start(widgets.search_entry, True, True, 0) 463 | vbox.pack_start(scrolled_window, True, True, 0) 464 | 465 | if not self.apps_: 466 | apps = get_app_info() 467 | for name, exec_cmd, icon in apps: 468 | item = create_submenu_item(name, icon, use_theme_icon=True, on_submenu_hover_enter=lambda w, e, a=self.active_submenu: on_submenu_hover_enter(w, e, a), on_submenu_hover_leave=on_submenu_hover_leave) 469 | item.exec_cmd = exec_cmd 470 | item.connect("button-press-event", lambda w, e, cmd=exec_cmd: on_submenu_item_click(w, e, lambda w: launch_app(w, cmd))) 471 | self.apps_[name] = exec_cmd 472 | widgets.listbox.add(item) 473 | 474 | if self.config.show_app_info: 475 | info = Gtk.Label(label = 'Use Tab and Shift+Tab to navigate.') 476 | info.get_style_context().add_class('App-Info') 477 | vbox.pack_start(info, False, False, 0) 478 | 479 | def navigate(direction): 480 | if direction == 'up' and window.current_selected_index > 0: 481 | select_item(window.current_selected_index - 1) 482 | elif direction == 'down' and window.current_selected_index < len(window.submenu_items) - 1: 483 | select_item(window.current_selected_index + 1) 484 | 485 | def select_item(index): 486 | if 0 <= window.current_selected_index < len(window.submenu_items): 487 | window.submenu_items[window.current_selected_index].get_style_context().remove_class("menu-item-hover") 488 | window.submenu_items[window.current_selected_index].unset_state_flags(Gtk.StateFlags.PRELIGHT) 489 | 490 | window.current_selected_index = index 491 | if 0 <= index < len(window.submenu_items): 492 | window.submenu_items[index].get_style_context().add_class("menu-item-hover") 493 | window.submenu_items[index].set_state_flags(Gtk.StateFlags.PRELIGHT, True) 494 | adjustment = scrolled_window.get_vadjustment() 495 | item_allocation = window.submenu_items[index].get_allocation() 496 | if item_allocation.y < adjustment.get_value(): 497 | adjustment.set_value(item_allocation.y) 498 | elif (item_allocation.y + item_allocation.height) > (adjustment.get_value() + adjustment.get_page_size()): 499 | adjustment.set_value(item_allocation.y + item_allocation.height - adjustment.get_page_size()) 500 | 501 | def activate_selected(): 502 | if 0 <= window.current_selected_index < len(window.submenu_items): 503 | item = window.submenu_items[window.current_selected_index] 504 | if hasattr(item, 'exec_cmd'): 505 | launch_app(item, item.exec_cmd) 506 | window.destroy() 507 | for w in Gtk.Window.list_toplevels(): 508 | w.destroy() 509 | Gtk.main_quit() 510 | 511 | window.navigate = navigate 512 | window.activate_selected = activate_selected 513 | 514 | return window 515 | 516 | return create_submenu 517 | 518 | def build_power_menu(self): 519 | def create_submenu(): 520 | window, vbox = build_submenu_window("Power Settings") 521 | 522 | items = [ 523 | ("Power Off ⏻", None, shutdown_machine), 524 | ("Reboot ⟳", None, reboot_machine), 525 | ("Lock 🗝", None, lock_machine), 526 | ("Hibernate ❄", None, hib_machine) 527 | ] 528 | 529 | window.submenu_items = [] 530 | window.current_selected_index = 0 531 | 532 | for label_text, icon_path, action_func in items: 533 | item = create_submenu_item(label_text, icon_path, on_submenu_hover_enter=lambda w, e, a=self.active_submenu: on_submenu_hover_enter(w, e, a), on_submenu_hover_leave=on_submenu_hover_leave) 534 | item.action_func = action_func 535 | item.connect("button-press-event", lambda w, e, f=action_func: on_submenu_item_click(w, e, f)) 536 | vbox.pack_start(item, False, False, 0) 537 | window.submenu_items.append(item) 538 | 539 | def navigate(direction): 540 | if direction == 'up' and window.current_selected_index > 0: 541 | select_item(window.current_selected_index - 1) 542 | elif direction == 'down' and window.current_selected_index < len(window.submenu_items) - 1: 543 | select_item(window.current_selected_index + 1) 544 | 545 | def select_item(index): 546 | if 0 <= window.current_selected_index < len(window.submenu_items): 547 | window.submenu_items[window.current_selected_index].get_style_context().remove_class("menu-item-hover") 548 | window.submenu_items[window.current_selected_index].unset_state_flags(Gtk.StateFlags.PRELIGHT) 549 | 550 | window.current_selected_index = index 551 | if 0 <= index < len(window.submenu_items): 552 | window.submenu_items[index].get_style_context().add_class("menu-item-hover") 553 | window.submenu_items[index].set_state_flags(Gtk.StateFlags.PRELIGHT, True) 554 | 555 | def activate_selected(): 556 | if 0 <= window.current_selected_index < len(window.submenu_items): 557 | item = window.submenu_items[window.current_selected_index] 558 | if hasattr(item, 'action_func'): 559 | item.action_func(item) 560 | window.destroy() 561 | for w in Gtk.Window.list_toplevels(): 562 | w.destroy() 563 | Gtk.main_quit() 564 | 565 | window.navigate = navigate 566 | window.activate_selected = activate_selected 567 | 568 | 569 | return window 570 | 571 | return create_submenu 572 | 573 | def build_system_info_menu(self): 574 | def create_submenu(): 575 | window, vbox = build_submenu_window("Hardware Info") 576 | 577 | cpu_header = create_info_header("CPU", "app_images/cpu.png") 578 | cpu_header.get_style_context().add_class('sysInfo') 579 | 580 | vbox.pack_start(cpu_header, False, False, 0) 581 | 582 | cpu_name = create_info_item(f"CPU Name: {get_cpu_info()}") 583 | cpu_name.get_style_context().add_class('sysInfo') 584 | 585 | vbox.pack_start(cpu_name, False, False, 0) 586 | 587 | vbox.pack_start(widgets.cpu_temp, False, False, 0) 588 | widgets.cpu_temp.set_margin_start(20) 589 | widgets.cpu_temp.set_xalign(0) 590 | 591 | vbox.pack_start(widgets.cpu_usage, False, False, 0) 592 | widgets.cpu_usage.set_margin_start(20) 593 | widgets.cpu_usage.set_xalign(0) 594 | 595 | vbox.pack_start(widgets.sys_separator, False, False, 4) 596 | 597 | ram_header = create_info_header("RAM", "app_images/ram.png") 598 | ram_header.get_style_context().add_class('sysInfo') 599 | 600 | vbox.pack_start(ram_header, False, False, 0) 601 | 602 | vbox.pack_start(widgets.ram_usage, False, False, 0) 603 | widgets.ram_usage.set_margin_start(20) 604 | widgets.ram_usage.set_xalign(0) 605 | 606 | vbox.pack_start(widgets.used_ram, False, False, 0) 607 | widgets.used_ram.set_margin_start(20) 608 | widgets.used_ram.set_xalign(0) 609 | 610 | check = check_gpu() 611 | if check != '': 612 | vbox.pack_start(widgets.sys_separator_, False, False, 4) 613 | 614 | gpu_header = create_info_header("GPU", "app_images/gpu.png") 615 | gpu_header.get_style_context().add_class('sysInfo') 616 | 617 | vbox.pack_start(gpu_header, False, False, 0) 618 | 619 | gpu_name = create_info_item(f"GPU Name: {get_nvidia_name()}") 620 | gpu_name.get_style_context().add_class('sysInfo') 621 | 622 | vbox.pack_start(gpu_name, False, False, 0) 623 | 624 | vbox.pack_start(widgets.gpu_temp, False, False, 0) 625 | widgets.gpu_temp.set_margin_start(20) 626 | widgets.gpu_temp.set_xalign(0) 627 | 628 | vbox.pack_start(widgets.gpu_usage, False, False, 0) 629 | widgets.gpu_usage.set_margin_start(20) 630 | widgets.gpu_usage.set_xalign(0) 631 | 632 | vbox.pack_start(widgets.gpu_vram, False, False, 0) 633 | widgets.gpu_vram.set_margin_start(20) 634 | widgets.gpu_vram.set_xalign(0) 635 | 636 | vbox.pack_start(widgets.gpu_speed, False, False, 0) 637 | widgets.gpu_speed.set_margin_start(20) 638 | widgets.gpu_speed.set_xalign(0) 639 | 640 | vbox.pack_start(widgets.gpu_power, False, False, 0) 641 | widgets.gpu_power.set_margin_start(20) 642 | widgets.gpu_power.set_xalign(0) 643 | 644 | window.navigate = lambda direction: None 645 | window.activate_selected = lambda: None 646 | 647 | return window 648 | 649 | return create_submenu 650 | 651 | 652 | def build_time_menu(self): 653 | def create_submenu(): 654 | window, vbox = build_submenu_window("Time & Date") 655 | vbox.pack_start(widgets.time_label, False, False, 0) 656 | vbox.pack_start(widgets.timer_separator, False, False, 0) 657 | # vbox.pack_start(widgets.date_label, False, False, 0) 658 | vbox.pack_start(widgets.rounded_calendar, False, False, 0) 659 | 660 | window.navigate = lambda direction: None 661 | window.activate_selected = lambda: None 662 | 663 | return window 664 | return create_submenu 665 | 666 | 667 | 668 | 669 | def show_menu(): 670 | css_provider = Gtk.CssProvider() 671 | with open ('config/style.css', 'r') as f: 672 | css = f.read() 673 | css_provider.load_from_data(css.encode()) 674 | 675 | Gtk.StyleContext.add_provider_for_screen( 676 | Gdk.Screen.get_default(), 677 | css_provider, 678 | Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION 679 | ) 680 | capsule = Capsule() 681 | Gtk.main() 682 | 683 | show_menu() -------------------------------------------------------------------------------- /app_images/app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NaturalCapsule/capsule/c654ad0e956a380d22ba39b1cfdffe949c6809f4/app_images/app.png -------------------------------------------------------------------------------- /app_images/clock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NaturalCapsule/capsule/c654ad0e956a380d22ba39b1cfdffe949c6809f4/app_images/clock.png -------------------------------------------------------------------------------- /app_images/cpu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NaturalCapsule/capsule/c654ad0e956a380d22ba39b1cfdffe949c6809f4/app_images/cpu.png -------------------------------------------------------------------------------- /app_images/exit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NaturalCapsule/capsule/c654ad0e956a380d22ba39b1cfdffe949c6809f4/app_images/exit.png -------------------------------------------------------------------------------- /app_images/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NaturalCapsule/capsule/c654ad0e956a380d22ba39b1cfdffe949c6809f4/app_images/folder.png -------------------------------------------------------------------------------- /app_images/gpu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NaturalCapsule/capsule/c654ad0e956a380d22ba39b1cfdffe949c6809f4/app_images/gpu.png -------------------------------------------------------------------------------- /app_images/info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NaturalCapsule/capsule/c654ad0e956a380d22ba39b1cfdffe949c6809f4/app_images/info.png -------------------------------------------------------------------------------- /app_images/music.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NaturalCapsule/capsule/c654ad0e956a380d22ba39b1cfdffe949c6809f4/app_images/music.png -------------------------------------------------------------------------------- /app_images/power.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NaturalCapsule/capsule/c654ad0e956a380d22ba39b1cfdffe949c6809f4/app_images/power.png -------------------------------------------------------------------------------- /app_images/ram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NaturalCapsule/capsule/c654ad0e956a380d22ba39b1cfdffe949c6809f4/app_images/ram.png -------------------------------------------------------------------------------- /app_images/terminal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NaturalCapsule/capsule/c654ad0e956a380d22ba39b1cfdffe949c6809f4/app_images/terminal.png -------------------------------------------------------------------------------- /app_info.py: -------------------------------------------------------------------------------- 1 | import re 2 | from pathlib import Path 3 | from configparser import ConfigParser 4 | 5 | desktop_dirs = [ 6 | Path("~/.local/share/applications").expanduser(), 7 | Path("/usr/share/applications") 8 | ] 9 | 10 | apps = [] 11 | 12 | def clean_exec(exec_cmd): 13 | return re.sub(r"\s*%[a-zA-Z]", "", exec_cmd).strip() 14 | 15 | def get_app_info(): 16 | for directory in desktop_dirs: 17 | if not directory.exists(): 18 | continue 19 | for file in directory.glob("*.desktop"): 20 | config = ConfigParser(interpolation=None, strict=False) 21 | config.read(file, encoding="utf-8") 22 | try: 23 | name = config.get("Desktop Entry", "Name") 24 | exec_cmd = config.get("Desktop Entry", "Exec", fallback="") 25 | icon = config.get("Desktop Entry", "Icon", fallback="") 26 | 27 | exec_cmd = clean_exec(exec_cmd) 28 | 29 | if name and exec_cmd: 30 | apps.append((name, exec_cmd, icon)) 31 | except Exception as e: 32 | print(f"Error reading {file}: {e}") 33 | return apps 34 | 35 | # apps = get_app_info() 36 | 37 | # for name, exec_cmd, icon in apps: 38 | # print(f"{name} | Exec: {exec_cmd} | Icon: {icon}") -------------------------------------------------------------------------------- /assistant.py: -------------------------------------------------------------------------------- 1 | import speech_recognition as sr 2 | import subprocess, shlex 3 | from app_info import * 4 | import os 5 | recognizer = sr.Recognizer() 6 | 7 | apps = {} 8 | 9 | ap = get_app_info() 10 | for name, exec, icon in get_app_info(): 11 | print(name.lower()) 12 | apps[name.lower()] = exec 13 | 14 | 15 | 16 | def listen_command(): 17 | try: 18 | with sr.Microphone() as source: 19 | print("Listening...") 20 | recognizer.adjust_for_ambient_noise(source) 21 | audio = recognizer.listen(source) 22 | command = recognizer.recognize_google(audio) 23 | os.system('clear') 24 | print(f"You said: {command.lower()}") 25 | return command.lower() 26 | except sr.UnknownValueError: 27 | print("Sorry, I did not understand that.") 28 | return "" 29 | except sr.RequestError: 30 | print("Could not request results; check your network connection.") 31 | return "" 32 | 33 | def execute_command(command): 34 | if command in list(apps.keys()): 35 | cmd = apps.get(command) 36 | if command == 'steam': 37 | cmd = apps.get('steam (runtime)') 38 | print(cmd) 39 | cmd = shlex.split(cmd) 40 | print(cmd) 41 | subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell = True, cwd=os.path.expanduser("~")) 42 | else: 43 | pass 44 | 45 | while True: 46 | command = listen_command() 47 | if command: 48 | execute_command(command) -------------------------------------------------------------------------------- /clicks.py: -------------------------------------------------------------------------------- 1 | import gi 2 | gi.require_version('Gtk', '3.0') 3 | from actions import * 4 | 5 | from gi.repository import Gtk, Gdk 6 | 7 | def on_exit_click(widget, event, destroy): 8 | if event is None or event.type == Gdk.EventType.BUTTON_PRESS: 9 | exit_(widget) 10 | destroy() 11 | Gtk.main_quit() 12 | 13 | def on_fileM_click(widget, event, destroy, file_manager): 14 | if event is None or event.type == Gdk.EventType.BUTTON_PRESS: 15 | open_fileM(widget, file_manager) 16 | destroy() 17 | Gtk.main_quit() 18 | 19 | def on_terminal_click(widget, event, destroy, terminal): 20 | if event is None or event.type == Gdk.EventType.BUTTON_PRESS: 21 | open_terminal(widget, terminal) 22 | destroy() 23 | Gtk.main_quit() 24 | 25 | def on_submenu_item_click(widget, event, callback): 26 | if event is None or event.type == Gdk.EventType.BUTTON_PRESS: 27 | callback(widget) 28 | for window in Gtk.Window.list_toplevels(): 29 | window.destroy() 30 | Gtk.main_quit() 31 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | from configparser import ConfigParser 2 | 3 | config = ConfigParser(interpolation = None) 4 | config.read('config/config.ini') 5 | 6 | use_blur = config.getboolean('Appearance', 'UseTransparentBlur') 7 | show_app_info = config.getboolean('Appearance', 'ShowApplicationInfo') 8 | 9 | terminal = config.get('Open Apps', 'terminal') 10 | file_manager = config.get('Open Apps', 'FileManager') 11 | 12 | display_time = config.get('Appearance', 'DisplayTime') 13 | -------------------------------------------------------------------------------- /config/config.ini: -------------------------------------------------------------------------------- 1 | [Appearance] 2 | UseTransparentBlur = false 3 | ShowApplicationInfo = true 4 | ; Check time.strftime https://www.geeksforgeeks.org/time-strftime-function-in-python/ 5 | DisplayTime =  %H:%M 6 | 7 | [Open Apps] 8 | terminal = kitty 9 | FileManager = pcmanfm -------------------------------------------------------------------------------- /config/style.css: -------------------------------------------------------------------------------- 1 | .window { 2 | background-color: #1e1e2f; 3 | color: #f8f8f2; 4 | border:3px solid #44475a; 5 | border-radius: 15px; 6 | } 7 | 8 | .Separators { 9 | background-color: white; 10 | min-height: 2px; 11 | margin: 20px; 12 | border-radius: 1px; 13 | } 14 | 15 | .Submenu-Button { 16 | color: white; 17 | } 18 | 19 | .sysInfo { 20 | color: #d0d0d0; 21 | font-weight: 500; 22 | font-size: 14px; 23 | margin-bottom: 5px; 24 | } 25 | 26 | .Media-Title { 27 | color: #ffffff; 28 | font-size: 20px; 29 | font-weight: bold; 30 | margin-bottom: 10px; 31 | margin: 5px; 32 | } 33 | 34 | .Media-Image { 35 | border-radius: 9px; 36 | margin: 5px; 37 | border: 2px solid #44475a; 38 | border-radius: 40px; 39 | } 40 | 41 | .Clock { 42 | padding-top: 10px; 43 | color: #d0d0d0; 44 | font-weight: 500; 45 | font-size: 30px; 46 | margin-bottom: 5px; 47 | } 48 | 49 | 50 | 51 | .current-day { 52 | background-color: white; 53 | color: #282a36; 54 | border-radius: 8px; 55 | padding: 2px 6px; 56 | margin: 1px; 57 | font-weight: bold; 58 | min-width: 20px; 59 | min-height: 20px; 60 | } 61 | 62 | .calendar-day { 63 | color: white; 64 | min-width: 20px; 65 | min-height: 20px; 66 | padding: 2px; 67 | /* padding: 2px 6px; */ 68 | } 69 | 70 | .weekday-header { 71 | color: white; 72 | font-weight: bold; 73 | padding: 2px 74 | } 75 | 76 | .month-header { 77 | font-weight: bold; 78 | font-size: 14px; 79 | padding: 5px; 80 | margin: 5px; 81 | /* padding-bottom: 5px */ 82 | } 83 | 84 | .App-Info { 85 | color: #f8f8f2; 86 | padding: 15px; 87 | border-radius: 10px; 88 | } 89 | 90 | .App-List-selected-row { 91 | background: linear-gradient(135deg, #8a2be2 0%, #4b0082 100%); 92 | color: white; 93 | border-radius: 12px; 94 | transition: background-color 0.3s ease; 95 | outline: none; 96 | } 97 | 98 | .App-List { 99 | background-color: #282a36; 100 | color: #f8f8f2; 101 | border: 2px solid #44475a; 102 | } 103 | 104 | .App-Search { 105 | background-color: #3b3f51; 106 | border-radius: 8px; 107 | padding: 5px; 108 | border: none; 109 | transition: background-color 0.3s ease, border 0.3s ease; 110 | border: 2px solid #44475a; 111 | } 112 | 113 | .App-Search:focus { 114 | color: white; 115 | background-color: #4e5266; 116 | border: 1px solid #6272a4; 117 | box-shadow: 0 0 5px rgba(98, 114, 164, 0.5); 118 | } 119 | 120 | .App-Search > image, 121 | .App-Search > icon.primary { 122 | color: white; 123 | } 124 | 125 | .menu-item-hover { 126 | border-radius: 12px; 127 | color: #ffffff; 128 | background-color: #ff79c6; 129 | animation: gradient_f 20s ease-in infinite; 130 | transition: background-color 0.3s cubic-bezier(.55,-0.68,.48,1.682); 131 | } 132 | 133 | .menu-item-hover:hover { 134 | background-color: #bd93f9; 135 | } 136 | 137 | .Scroll-Window { 138 | background-color: #1e1e2f; 139 | border-radius: 10px; 140 | } 141 | 142 | .Submenu { 143 | background-color: #2e2e3e; 144 | color: #f8f8f2; 145 | border-radius: 10px; 146 | border:3px solid #44475a; 147 | } -------------------------------------------------------------------------------- /date.py: -------------------------------------------------------------------------------- 1 | import calendar 2 | from datetime import datetime 3 | import gi 4 | gi.require_version('Gtk', '3.0') 5 | from gi.repository import Gtk, Gdk 6 | 7 | class RoundedCalendarLabel(Gtk.Box): 8 | def __init__(self): 9 | super().__init__(orientation=Gtk.Orientation.VERTICAL) 10 | self.set_homogeneous(False) 11 | 12 | self.apply_css() 13 | 14 | self.create_calendar() 15 | 16 | def apply_css(self): 17 | css_provider = Gtk.CssProvider() 18 | with open ('config/style.css', 'r') as f: 19 | css = f.read() 20 | css_provider.load_from_data(css.encode()) 21 | 22 | Gtk.StyleContext.add_provider_for_screen( 23 | Gdk.Screen.get_default(), 24 | css_provider, 25 | Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION 26 | ) 27 | 28 | def create_calendar(self): 29 | today = datetime.today() 30 | current_year = today.year 31 | current_month = today.month 32 | current_day = today.day 33 | 34 | header_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) 35 | header_box.set_halign(Gtk.Align.CENTER) 36 | header_label = Gtk.Label(label=f"{calendar.month_name[current_month]} {current_year}") 37 | header_label.get_style_context().add_class("month-header") 38 | header_box.pack_start(header_label, False, False, 0) 39 | self.pack_start(header_box, False, False, 0) 40 | 41 | weekdays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] 42 | weekday_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) 43 | weekday_box.set_halign(Gtk.Align.CENTER) 44 | for day in weekdays: 45 | label = Gtk.Label(label=day) 46 | label.get_style_context().add_class("weekday-header") 47 | label.set_size_request(30, -1) 48 | weekday_box.pack_start(label, False, False, 0) 49 | self.pack_start(weekday_box, False, False, 0) 50 | 51 | first_day_of_month = datetime(current_year, current_month, 1) 52 | first_weekday = first_day_of_month.weekday() 53 | first_weekday = (first_weekday + 1) % 7 54 | days_in_month = (datetime(current_year, current_month + 1, 1) - first_day_of_month).days 55 | 56 | day_counter = 1 57 | for week in range(6): 58 | week_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) 59 | week_box.set_halign(Gtk.Align.CENTER) 60 | 61 | for day_of_week in range(7): 62 | if week == 0 and day_of_week < first_weekday: 63 | label = Gtk.Label() 64 | label.set_size_request(30, 25) 65 | elif day_counter <= days_in_month: 66 | label = Gtk.Label(label=str(day_counter)) 67 | label.set_size_request(30, 25) 68 | 69 | if day_counter == current_day: 70 | label.get_style_context().add_class("current-day") 71 | else: 72 | label.get_style_context().add_class("calendar-day") 73 | 74 | day_counter += 1 75 | else: 76 | label = Gtk.Label() 77 | label.set_size_request(30, 25) 78 | 79 | week_box.pack_start(label, False, False, 0) 80 | 81 | self.pack_start(week_box, False, False, 0) 82 | 83 | if day_counter > days_in_month: 84 | break 85 | 86 | def update_calendar(self): 87 | for child in self.get_children(): 88 | self.remove(child) 89 | 90 | self.create_calendar() 91 | self.show_all() 92 | return True -------------------------------------------------------------------------------- /effects.py: -------------------------------------------------------------------------------- 1 | import gi 2 | gi.require_version('Gtk', '3.0') 3 | gi.require_version('Gdk', '3.0') 4 | 5 | from gi.repository import Gtk, Gdk 6 | 7 | def on_submenu_hover_leave(widget, event): 8 | widget.get_style_context().remove_class("menu-item-hover") 9 | widget.unset_state_flags(Gtk.StateFlags.PRELIGHT) 10 | return False 11 | 12 | def on_submenu_hover_enter(widget, event, active_submenu): 13 | widget.get_style_context().add_class("menu-item-hover") 14 | widget.set_state_flags(Gtk.StateFlags.PRELIGHT, True) 15 | 16 | if active_submenu and hasattr(active_submenu, 'submenu_items'): 17 | if widget in active_submenu.submenu_items: 18 | active_submenu.current_selected_index = active_submenu.submenu_items.index(widget) 19 | 20 | return False 21 | 22 | def on_hover_leave(widget, event): 23 | widget.get_style_context().remove_class("menu-item-hover") 24 | widget.unset_state_flags(Gtk.StateFlags.PRELIGHT) 25 | return False -------------------------------------------------------------------------------- /items.py: -------------------------------------------------------------------------------- 1 | import gi 2 | 3 | gi.require_version('Gtk', '3.0') 4 | gi.require_version('Gdk', '3.0') 5 | from gi.repository import Gtk, GdkPixbuf, GLib, Gdk 6 | 7 | 8 | def create_menu_item(label_text, icon_path, on_hover_enter, on_hover_leave): 9 | box = Gtk.EventBox() 10 | box.set_above_child(False) 11 | 12 | hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) 13 | hbox.set_border_width(8) 14 | 15 | try: 16 | pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(icon_path, 20, 20) 17 | icon = Gtk.Image.new_from_pixbuf(pixbuf) 18 | hbox.pack_start(icon, False, False, 2) 19 | except GLib.Error: 20 | pass 21 | 22 | label = Gtk.Label(label=label_text) 23 | label.set_xalign(0) 24 | hbox.pack_start(label, True, True, 2) 25 | 26 | if label_text != "Terminal" and label_text != "Exit" and label_text != "Open File Manager": 27 | arrow = Gtk.Image.new_from_icon_name("pan-end-symbolic", Gtk.IconSize.MENU) 28 | hbox.pack_end(arrow, False, False, 2) 29 | 30 | box.add(hbox) 31 | 32 | box.connect("enter-notify-event", on_hover_enter) 33 | box.connect("leave-notify-event", on_hover_leave) 34 | 35 | return box 36 | 37 | 38 | def build_submenu_window(title): 39 | window = Gtk.Window(type=Gtk.WindowType.POPUP) 40 | window.set_decorated(False) 41 | window.set_resizable(False) 42 | window.set_skip_taskbar_hint(True) 43 | window.set_skip_pager_hint(True) 44 | window.get_style_context().add_class('Submenu') 45 | 46 | screen = window.get_screen() 47 | visual = screen.get_rgba_visual() 48 | if visual and screen.is_composited(): 49 | window.set_visual(visual) 50 | 51 | vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) 52 | window.add(vbox) 53 | 54 | window.connect("key-press-event", lambda w, e: w.destroy() if e.keyval == Gdk.KEY_Escape else None) 55 | 56 | return window, vbox 57 | 58 | 59 | def create_submenu_item(label_text, icon_path=None, use_theme_icon=False, on_submenu_hover_enter = None, on_submenu_hover_leave = None): 60 | box = Gtk.EventBox() 61 | box.set_above_child(False) 62 | 63 | hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) 64 | hbox.set_border_width(8) 65 | 66 | if icon_path: 67 | if use_theme_icon: 68 | try: 69 | pixbuf = Gtk.IconTheme.get_default().load_icon(icon_path, 32, 0) 70 | scaled_pixbuf = pixbuf.scale_simple(20, 20, GdkPixbuf.InterpType.BILINEAR) 71 | icon = Gtk.Image.new_from_pixbuf(scaled_pixbuf) 72 | except GLib.Error: 73 | icon = Gtk.Image.new_from_icon_name(icon_path, Gtk.IconSize.SMALL_TOOLBAR) 74 | else: 75 | try: 76 | pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(icon_path, 20, 20) 77 | icon = Gtk.Image.new_from_pixbuf(pixbuf) 78 | except GLib.Error: 79 | icon = Gtk.Image.new_from_icon_name(icon_path, Gtk.IconSize.MENU) 80 | 81 | hbox.pack_start(icon, False, False, 2) 82 | 83 | label = Gtk.Label(label=label_text) 84 | label.get_style_context().add_class('Submenu-Button') # yeah this is a label but it acts like a button so its a button, shut up 85 | label.set_xalign(0) 86 | hbox.pack_start(label, True, True, 2) 87 | 88 | box.add(hbox) 89 | 90 | box.connect("enter-notify-event", on_submenu_hover_enter) 91 | box.connect("leave-notify-event", on_submenu_hover_leave) 92 | 93 | return box 94 | 95 | 96 | def create_info_header(text, icon_path): 97 | box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) 98 | box.set_border_width(8) 99 | 100 | try: 101 | pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(icon_path, 20, 20) 102 | icon = Gtk.Image.new_from_pixbuf(pixbuf) 103 | box.pack_start(icon, False, False, 0) 104 | except GLib.Error: 105 | pass 106 | 107 | label = Gtk.Label(label=text) 108 | label.set_xalign(0) 109 | box.pack_start(label, False, False, 0) 110 | 111 | return box 112 | 113 | def create_info_item(text): 114 | label = Gtk.Label(label=text) 115 | label.set_xalign(0) 116 | label.set_margin_start(20) 117 | 118 | return label -------------------------------------------------------------------------------- /media.py: -------------------------------------------------------------------------------- 1 | import dbus 2 | from time import sleep 3 | import subprocess 4 | import gi 5 | gi.require_version('Gtk', '3.0') 6 | 7 | class MediaPlayerMonitor: 8 | def __init__(self): 9 | self.session_bus = dbus.SessionBus() 10 | self.players = {} 11 | self.current_player = None 12 | 13 | self.title_ = '' 14 | self.artist = '' 15 | self._album = '' 16 | self.psoition = '' 17 | self.playback_status = '' 18 | self.art_url = '' 19 | self.monitor() 20 | 21 | def get_players(self): 22 | for service in self.session_bus.list_names(): 23 | if service.startswith("org.mpris.MediaPlayer2."): 24 | if service not in self.players: 25 | self.players[service] = self.session_bus.get_object(service, "/org/mpris/MediaPlayer2") 26 | return self.players 27 | 28 | def get_player_properties(self, player): 29 | try: 30 | # iface = dbus.Interface(player, 'org.freedesktop.DBus.Properties') 31 | iface = dbus.Interface(player, 'org.freedesktop.DBus.Properties') 32 | metadata = iface.Get('org.mpris.MediaPlayer2.Player', 'Metadata') 33 | playback_status = iface.Get('org.mpris.MediaPlayer2.Player', 'PlaybackStatus') 34 | position = iface.Get('org.mpris.MediaPlayer2.Player', 'Position') 35 | title = metadata.get('xesam:title', 'Unknown Title') 36 | artist = ', '.join(metadata.get('xesam:artist', [])) 37 | album = metadata.get('xesam:album', 'Unknown Album') 38 | art_url = metadata.get('mpris:artUrl', '') 39 | 40 | return { 41 | 'title': title, 42 | 'artist': artist, 43 | 'album': album, 44 | 'art_url': art_url, 45 | 'playback_status': playback_status, 46 | 'position': position // 1_000_000 47 | } 48 | except dbus.exceptions.DBusException as e: 49 | return None 50 | 51 | 52 | def update_current_player(self): 53 | active_found = False 54 | for service, player in self.players.items(): 55 | properties = self.get_player_properties(player) 56 | if properties: 57 | if properties['playback_status'] == 'Playing' or properties['playback_status'] == 'Paused': 58 | self.current_player = player 59 | active_found = True 60 | break 61 | if not active_found: 62 | self.current_player = player 63 | if not active_found: 64 | self.current_player = None 65 | 66 | def monitor(self): 67 | self.get_players() 68 | self.update_current_player() 69 | 70 | if self.current_player: 71 | retry = 0 72 | while retry < 3: 73 | self.properties = self.get_player_properties(self.current_player) 74 | if self.properties and self.properties['title'] != 'Unknown Title': 75 | break 76 | else: 77 | sleep(0.3) 78 | retry += 1 79 | 80 | if self.properties: 81 | self.title_ = f"{self.properties['title']}" 82 | self.artist = f"{self.properties['artist']}" 83 | self._album = f"{self.properties['album']}" 84 | self.psoition = f"{self.properties['position']}" 85 | self.playback_status = f"{self.properties['playback_status']}" 86 | self.art_url = f"{self.properties['art_url']}" 87 | else: 88 | self.title_ = '' 89 | self.artist = '' 90 | self._album = '' 91 | self.psoition = '' 92 | self.playback_status = '' 93 | self.art_url = '' 94 | else: 95 | self.title_ = '' 96 | self.artist = '' 97 | self._album = '' 98 | self.psoition = '' 99 | self.playback_status = '' 100 | self.art_url = '' -------------------------------------------------------------------------------- /raduis_image.py: -------------------------------------------------------------------------------- 1 | import gi 2 | gi.require_version('Gdk', '3.0') 3 | from gi.repository import Gdk 4 | 5 | 6 | import cairo 7 | 8 | # def create_circular_pixbuf(pixbuf): 9 | # width, height = pixbuf.get_width(), pixbuf.get_height() 10 | # radius = min(width, height) // 2 11 | 12 | # surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) 13 | # ctx = cairo.Context(surface) 14 | 15 | # ctx.set_source_rgba(0, 0, 0, 0) 16 | # ctx.set_operator(cairo.Operator.SOURCE) 17 | # ctx.paint() 18 | 19 | # ctx.set_operator(cairo.Operator.OVER) 20 | # ctx.arc(width // 2, height // 2, radius, 0, 2 * 3.1416) 21 | # ctx.clip() 22 | 23 | # gdk_cairo = Gdk.cairo_surface_create_from_pixbuf(pixbuf, 0, None) 24 | # ctx.set_source_surface(gdk_cairo, 0, 0) 25 | # ctx.paint() 26 | 27 | # return Gdk.pixbuf_get_from_surface(surface, 0, 0, width, height) 28 | 29 | def create_radius_pixbuf(pixbuf): 30 | width, height = pixbuf.get_width(), pixbuf.get_height() 31 | 32 | corner_radius = 30 33 | 34 | surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) 35 | ctx = cairo.Context(surface) 36 | 37 | ctx.set_source_rgba(0, 0, 0, 0) 38 | ctx.set_operator(cairo.Operator.SOURCE) 39 | ctx.paint() 40 | 41 | ctx.set_operator(cairo.Operator.OVER) 42 | ctx.move_to(corner_radius, 0) 43 | ctx.line_to(width - corner_radius, 0) 44 | ctx.arc(width - corner_radius, corner_radius, corner_radius, 3 * 3.1416 / 2, 2 * 3.1416) 45 | ctx.line_to(width, height - corner_radius) 46 | ctx.arc(width - corner_radius, height - corner_radius, corner_radius, 0, 3.1416 / 2) 47 | ctx.line_to(corner_radius, height) 48 | ctx.arc(corner_radius, height - corner_radius, corner_radius, 3.1416 / 2, 3.1416) 49 | ctx.line_to(0, corner_radius) 50 | ctx.arc(corner_radius, corner_radius, corner_radius, 3.1416, 3 * 3.1416 / 2) 51 | ctx.close_path() 52 | 53 | bite_radius = corner_radius // 2 54 | bite_angle_start = 1.5 55 | bite_angle_end = 2.0 56 | 57 | ctx.arc(corner_radius, corner_radius, bite_radius, bite_angle_start * 3.1416, bite_angle_end * 3.1416) 58 | ctx.line_to(corner_radius, corner_radius) 59 | 60 | ctx.clip() 61 | 62 | gdk_cairo = Gdk.cairo_surface_create_from_pixbuf(pixbuf, 0, None) 63 | ctx.set_source_surface(gdk_cairo, 0, 0) 64 | 65 | ctx.paint() 66 | 67 | return Gdk.pixbuf_get_from_surface(surface, 0, 0, width, height) -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # 💊 Capsule 2 | 3 | > ![App](repo_images/capsule.png) 4 | 5 | **Capsule** is a modern, customizable system panel built for Linux using Python, GTK and D-Bus. It provides essential system info, media control, and more — all in a clean, responsive UI. 6 | 7 | --- 8 | 9 | ## ✨ Features 10 | 11 | - 🚀 **Application launcher**: shows all the application that are installed on your system which you can launch 12 | - ⚡ **Power Options**: Lock, reboot, shutdown, or hibernate 13 | - 🎵 **Media Controls**: 14 | - Displays the current playing media title 15 | - Click to reveal title, artist, album art, and control buttons (play/pause, forward, backward, reset) 16 | - Thumbnail image auto-updates and appears as a rounded preview 17 | - 🖥️ **Hardware Info**: shows CPU, GPU and RAM usages temps, names, etc 18 | - 💻 **Terminal**: Lauch your favorite terminal emulator 19 | - 📂 **File Manager**: Lauch your favorite file manager 20 | - 🎨 **Custom Styling**: Easily modify the look and feel via `config/style.css` 21 | --- 22 | 23 | 24 | ## 📸 Screenshots 25 | 26 | > ![ScreenShots](repo_images/2.png) ![ScreenShots](repo_images/1.png) ![ScreenShots](repo_images/3.png) ![ScreenShots](repo_images/4.png) ![ScreenShots](repo_images/5.png) 27 | 28 | --- 29 | 30 | ## 🛠️ Built With 31 | 32 | - **Python 3** 33 | - **GTK** – For GUI components 34 | - **D-Bus** – For Media title and Thumbnail 35 | 36 | --- 37 | 38 | 39 | ## 📦 Installation 40 | 41 | make sure these packages are installed on your system 42 | `sudo pacman -S python-gobject gtk3 playerctl hyprlock` 43 | and 44 | `pip install pyGObject pycairo dbus-python configparser` 45 | 46 | 1. **Clone the repo** 47 | ```bash 48 | git clone https://github.com/NaturalCapsule/capsule 49 | ``` 50 | 51 | 2. **Goto directory** 52 | ```bash 53 | cd capsule 54 | ``` 55 | 56 | 3. **Launch** 57 | 58 | ```bash 59 | GDK_BACKEND=wayland python app.py 60 | ``` 61 | 62 | 4. **Enjoy!** 63 | -------------------------------------------------------------------------------- /repo_images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NaturalCapsule/capsule/c654ad0e956a380d22ba39b1cfdffe949c6809f4/repo_images/1.png -------------------------------------------------------------------------------- /repo_images/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NaturalCapsule/capsule/c654ad0e956a380d22ba39b1cfdffe949c6809f4/repo_images/2.png -------------------------------------------------------------------------------- /repo_images/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NaturalCapsule/capsule/c654ad0e956a380d22ba39b1cfdffe949c6809f4/repo_images/3.png -------------------------------------------------------------------------------- /repo_images/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NaturalCapsule/capsule/c654ad0e956a380d22ba39b1cfdffe949c6809f4/repo_images/4.png -------------------------------------------------------------------------------- /repo_images/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NaturalCapsule/capsule/c654ad0e956a380d22ba39b1cfdffe949c6809f4/repo_images/5.png -------------------------------------------------------------------------------- /repo_images/capsule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NaturalCapsule/capsule/c654ad0e956a380d22ba39b1cfdffe949c6809f4/repo_images/capsule.png -------------------------------------------------------------------------------- /sys_info.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import re 3 | 4 | 5 | def check_gpu(): 6 | try: 7 | result = subprocess.run(['nvidia-smi'], 8 | stdout=subprocess.PIPE, text=True) 9 | check = result.stdout.strip() 10 | return f"{check}" 11 | except FileNotFoundError: 12 | return '' 13 | 14 | def get_nvidia_gpu_usage(): 15 | try: 16 | result = subprocess.run(['nvidia-smi', '--query-gpu=utilization.gpu', '--format=csv,noheader,nounits'], 17 | stdout=subprocess.PIPE, text=True) 18 | usage = result.stdout.strip() 19 | return f"{usage}" 20 | except FileNotFoundError: 21 | return '' 22 | 23 | def get_nvidia_total_vram(): 24 | try: 25 | result = subprocess.run(['nvidia-smi', '--query-gpu=memory.total', '--format=csv,noheader,nounits'] 26 | , 27 | stdout=subprocess.PIPE, text=True) 28 | vram = result.stdout.strip() 29 | return f"{vram:.1}" 30 | except FileNotFoundError: 31 | return '' 32 | 33 | def get_nvidia_used_vram(): 34 | try: 35 | result = subprocess.run(['nvidia-smi', '--query-gpu=memory.used', '--format=csv,noheader,nounits'] 36 | , 37 | stdout=subprocess.PIPE, text=True) 38 | used_vram = result.stdout.strip() 39 | used_vram = int(used_vram) / 1000 40 | return f"{used_vram:.2f}" 41 | except FileNotFoundError: 42 | return '' 43 | 44 | def get_nvidia_temp(): 45 | try: 46 | result = subprocess.run(['nvidia-smi', '--query-gpu=temperature.gpu', '--format=csv,noheader,nounits'] 47 | , 48 | stdout=subprocess.PIPE, text=True) 49 | temp = result.stdout.strip() 50 | return f"{temp}" 51 | except FileNotFoundError: 52 | return '' 53 | 54 | def get_nvidia_powerdraw(): 55 | try: 56 | result = subprocess.run(['nvidia-smi', '--query-gpu=power.draw', '--format=csv,noheader,nounits'] 57 | , 58 | stdout=subprocess.PIPE, text=True) 59 | power = result.stdout.strip() 60 | return f"{power}W" 61 | except FileNotFoundError: 62 | return '' 63 | 64 | def get_nvidia_name(): 65 | try: 66 | result = subprocess.run(['nvidia-smi', '--query-gpu=name', '--format=csv,noheader,nounits'] 67 | , 68 | stdout=subprocess.PIPE, text=True) 69 | name = result.stdout.strip() 70 | return f"{name}" 71 | except FileNotFoundError: 72 | return '' 73 | 74 | def get_nvidia_fanspeed(): 75 | try: 76 | result = subprocess.run(['nvidia-smi', '--query-gpu=fan.speed', '--format=csv,noheader,nounits'] 77 | , 78 | stdout=subprocess.PIPE, text=True) 79 | fan = result.stdout.strip() 80 | return f"{fan}" 81 | except FileNotFoundError: 82 | return '' 83 | 84 | 85 | def get_total_ram(): 86 | result = subprocess.run(['grep', 'MemTotal', '/proc/meminfo'], stdout=subprocess.PIPE) 87 | total_ram_kb = int(result.stdout.decode().split()[1]) 88 | total_ram_gb = round(total_ram_kb / 1024 / 1024, 2) 89 | return f"{total_ram_gb}" 90 | 91 | 92 | def get_ram_usage(): 93 | result = subprocess.run( 94 | ['awk', '/MemTotal/ {total=$2} /MemAvailable/ {avail=$2} END {print (total-avail)/1024/1024}', '/proc/meminfo'], 95 | capture_output=True, 96 | text=True 97 | ) 98 | result = float(result.stdout.strip()) 99 | return f"{result:.3f}" 100 | 101 | def get_used_ram(): 102 | result = subprocess.run( 103 | [ 104 | 'awk', 105 | '/MemTotal|MemFree|Buffers|^Cached|^SReclaimable/ {a[$1]=$2} ' 106 | 'END {used=(a["MemTotal:"]-a["MemFree:"]-a["Buffers:"]-a["Cached:"]-a["SReclaimable:"])/1024/1024; ' 107 | 'printf "%.2f", used}' 108 | , 109 | '/proc/meminfo' 110 | ], 111 | capture_output=True, 112 | text=True 113 | ) 114 | 115 | result = float(result.stdout.strip()) 116 | return f"{result:.2f}" 117 | 118 | 119 | def get_cpu_temp(): 120 | result = subprocess.run(['sensors'], capture_output=True, text=True) 121 | temps = [] 122 | 123 | # Regex patterns for Intel & AMD 124 | patterns = [ 125 | r'(Core \d+):\s+\+([\d\.]+)°C', 126 | r'Package id \d+:\s+\+([\d\.]+)°C', 127 | r'Tctl:\s+\+([\d\.]+)°C', 128 | r'Tdie:\s+\+([\d\.]+)°C', 129 | ] 130 | 131 | for line in result.stdout.splitlines(): 132 | for pattern in patterns: 133 | match = re.search(pattern, line) 134 | if match: 135 | if 'Core' in pattern: 136 | core = match.group(1) 137 | temp = match.group(2) 138 | temps.append(f"{core}: {temp}°C") 139 | else: 140 | temp = match.group(1) 141 | temps.append(f"{temp}") 142 | 143 | if temps: 144 | return temps[0] 145 | else: 146 | return ["CPU temperature not found"] 147 | 148 | def get_cpu_usage(): 149 | result = subprocess.run(['top', '-bn1'], capture_output=True, text=True) 150 | for line in result.stdout.splitlines(): 151 | if line.startswith('%Cpu(s)'): 152 | parts = line.split(',') 153 | user = float(parts[0].split()[1]) 154 | system = float(parts[1].split()[0]) 155 | total_usage = user + system 156 | return f"{total_usage:.1f}" 157 | 158 | def get_cpu_info(): 159 | result = subprocess.run(['lscpu'], capture_output=True, text=True) 160 | for line in result.stdout.splitlines(): 161 | if line.startswith('Model name'): 162 | return line.split(":")[1].strip() -------------------------------------------------------------------------------- /timers.py: -------------------------------------------------------------------------------- 1 | import gi 2 | gi.require_version('Gtk', '3.0') 3 | from gi.repository import Gtk, GLib 4 | import threading 5 | from sys_info import * 6 | from updates import update_media, update_time, update_rounded_calendar 7 | 8 | def update_media_(label, image): 9 | GLib.timeout_add(400, update_media, label, image) 10 | 11 | def update_time_(label): 12 | GLib.timeout_add(1, update_time, label) 13 | 14 | def update_date_(label): 15 | GLib.timeout_add(1000, update_rounded_calendar, label) 16 | 17 | class RunTimers: 18 | def __init__(self, cpu_temp, cpu_usage, ram_usage, used_ram, gpu_temp=None, 19 | gpu_usage=None, gpu_vram=None, gpu_speed=None, gpu_power=None): 20 | self.cpu_temp = cpu_temp 21 | self.cpu_usage = cpu_usage 22 | self.ram_usage = ram_usage 23 | self.used_ram = used_ram 24 | self.gpu_temp = gpu_temp 25 | self.gpu_usage = gpu_usage 26 | self.gpu_vram = gpu_vram 27 | self.gpu_speed = gpu_speed 28 | self.gpu_power = gpu_power 29 | 30 | 31 | self.prev_values = {} 32 | 33 | GLib.timeout_add(2000, self.update_all_stats) 34 | 35 | def update_all_stats(self): 36 | thread = threading.Thread(target=self.collect_info) 37 | thread.daemon = True 38 | thread.start() 39 | return True 40 | 41 | def collect_info(self): 42 | cpu_temp_val = get_cpu_temp() 43 | cpu_usage_val = get_cpu_usage() 44 | ram_usage_val = get_ram_usage() 45 | used_ram_val = get_used_ram() 46 | 47 | 48 | if hasattr(self, 'gpu_temp') and self.gpu_temp is not None: 49 | gpu_temp_val = get_nvidia_temp() 50 | gpu_usage_val = get_nvidia_gpu_usage() 51 | gpu_vram_val = get_nvidia_used_vram() 52 | gpu_speed_val = get_nvidia_fanspeed() 53 | gpu_power_val = get_nvidia_powerdraw() 54 | else: 55 | gpu_temp_val = gpu_usage_val = gpu_vram_val = gpu_speed_val = gpu_power_val = None 56 | 57 | GLib.idle_add(self.update_ui, 58 | cpu_temp_val, cpu_usage_val, ram_usage_val, used_ram_val, 59 | gpu_temp_val, gpu_usage_val, gpu_vram_val, gpu_speed_val, gpu_power_val) 60 | 61 | def update_ui(self, cpu_temp_val, cpu_usage_val, ram_usage_val, used_ram_val, 62 | gpu_temp_val, gpu_usage_val, gpu_vram_val, gpu_speed_val, gpu_power_val): 63 | if self.should_update('cpu_temp', cpu_temp_val, 2): 64 | self.cpu_temp.set_label(f"CPU Temperature: {cpu_temp_val}°C") 65 | 66 | if self.should_update('cpu_usage', cpu_usage_val, 0.01): 67 | self.cpu_usage.set_label(f"CPU Usage: {cpu_usage_val}%") 68 | 69 | if self.should_update('ram_usage', ram_usage_val, 0.01): 70 | self.ram_usage.set_label(f"RAM Usage: {ram_usage_val}%") 71 | 72 | if self.should_update('used_ram', used_ram_val, 0.01): 73 | self.used_ram.set_label(f"Used RAM: {used_ram_val}GB/{get_total_ram()}GB") 74 | 75 | if self.gpu_temp is not None: 76 | if self.should_update('gpu_temp', gpu_temp_val, 2): 77 | self.gpu_temp.set_label(f"GPU Temperature: {gpu_temp_val}°C") 78 | 79 | if self.should_update('gpu_usage', gpu_usage_val, 1): 80 | self.gpu_usage.set_label(f"GPU Usage: {gpu_usage_val}%") 81 | 82 | if self.should_update('gpu_vram', gpu_vram_val, 0.01): 83 | self.gpu_vram.set_label(f"GPU VRAM: {gpu_vram_val}GB/{get_nvidia_total_vram()}GB") 84 | 85 | if self.should_update('gpu_speed', gpu_speed_val, 2): 86 | self.gpu_speed.set_label(f"GPU Fan Speed: {gpu_speed_val} RPM") 87 | 88 | if self.should_update('gpu_power', gpu_power_val, 3): 89 | self.gpu_power.set_label(f"GPU Power: {gpu_power_val}") 90 | 91 | return False 92 | 93 | def should_update(self, key, new_value, threshold): 94 | if key not in self.prev_values: 95 | self.prev_values[key] = new_value 96 | return True 97 | 98 | try: 99 | if abs(float(new_value) - float(self.prev_values[key])) >= threshold: 100 | self.prev_values[key] = new_value 101 | return True 102 | except (ValueError, TypeError): 103 | if new_value != self.prev_values[key]: 104 | self.prev_values[key] = new_value 105 | return True 106 | 107 | return False -------------------------------------------------------------------------------- /updates.py: -------------------------------------------------------------------------------- 1 | import gi 2 | import os 3 | import hashlib 4 | import urllib.request 5 | import time 6 | import config 7 | 8 | gi.require_version('Gdk', '3.0') 9 | from gi.repository import GdkPixbuf, GLib 10 | 11 | from media import MediaPlayerMonitor 12 | from sys_info import * 13 | from raduis_image import create_radius_pixbuf 14 | # from date import get_calendar_html 15 | 16 | media = MediaPlayerMonitor() 17 | 18 | title_name, player_name = None, None 19 | 20 | 21 | def update_cpu_temp(cpu_temp): 22 | temp = get_cpu_temp() 23 | cpu_temp.set_label(f"CPU Temp: {temp}C") 24 | return True 25 | 26 | def update_time(time_label): 27 | t = time.localtime() 28 | time__ = config.display_time 29 | fmt_time = time.strftime(time__, t) 30 | time_label.set_text(f"{fmt_time}") 31 | return True 32 | 33 | 34 | def update_rounded_calendar(calendar_widget): 35 | # for child in calendar_widget.get_children(): 36 | # calendar_widget.remove(child) 37 | 38 | # calendar_widget.create_calendar() 39 | # calendar_widget.show_all() 40 | # return True 41 | calendar_widget.update_calendar() 42 | return True 43 | 44 | def update_cpu_usage(cpu_usage): 45 | usage = get_cpu_usage() 46 | cpu_usage.set_label(f"CPU Usage: {usage}%") 47 | return True 48 | 49 | def update_ram_usage(ram_usage): 50 | usage = get_ram_usage() 51 | ram_usage.set_label(f"RAM Usage: {usage}%") 52 | return True 53 | 54 | def update_ram_used(ram_used): 55 | used = get_used_ram() 56 | total = get_total_ram() 57 | ram_used.set_label(f"Total RAM: {used}GB/{total}GB") 58 | return True 59 | 60 | def update_nvidia_temp(gpu_temp): 61 | temp = get_nvidia_temp() 62 | gpu_temp.set_label(f"GPU Temp: {temp}C") 63 | return True 64 | 65 | def update_nvidia_usage(gpu_usage): 66 | usage = get_nvidia_gpu_usage() 67 | gpu_usage.set_label(f"GPU Usage: {usage}%") 68 | return True 69 | 70 | def update_nvidia_fanspeed(gpu_speed): 71 | speed = get_nvidia_fanspeed() 72 | gpu_speed.set_label(f"GPU Fan Speed: {speed}RPM") 73 | return True 74 | 75 | def update_nvidia_usedVram(gpu_vram): 76 | used = get_nvidia_used_vram() 77 | total = get_nvidia_total_vram() 78 | gpu_vram.set_label(f"GPU VRAM: {used}GB/{total}GB") 79 | return True 80 | 81 | def update_nvidia_powerdraw(gpu_power): 82 | power = get_nvidia_powerdraw() 83 | gpu_power.set_label(f"GPU Power Draw: {power}") 84 | return True 85 | 86 | def get_cached_filename(title): 87 | hashed = hashlib.md5(title.encode()).hexdigest() 88 | return f"/tmp/{hashed}.jpg" 89 | 90 | 91 | def safe_set_label(label, text): 92 | GLib.idle_add(label.set_text, text) 93 | 94 | def safe_set_image(image_widget, pixbuf): 95 | GLib.idle_add(image_widget.set_from_pixbuf, pixbuf) 96 | 97 | def update_media(title_label, image): 98 | global title_name, player_name 99 | media.monitor() 100 | thumbnail = None 101 | 102 | current_player = media.current_player or '' 103 | current_title = media.title_ or '' 104 | should_update = False 105 | 106 | if player_name != current_player: 107 | print(f"Player changed: {player_name} → {current_player}") 108 | player_name = current_player 109 | should_update = True 110 | 111 | if title_name != current_title: 112 | print(f"Title changed: {title_name} → {current_title}") 113 | title_name = current_title 114 | should_update = True 115 | 116 | if current_player: 117 | try: 118 | if should_update: 119 | print(f"title: {current_title}\nartist: {media.artist}") 120 | 121 | safe_set_label(title_label, current_title) 122 | 123 | media_ = f'{current_title}\nBy\n{media.artist}' 124 | height, width = 120, 120 125 | if 'file:///' in media.art_url: 126 | thumbnail = media.art_url.replace('file:///', '/') 127 | height, width = 120, 120 128 | elif 'https://' in media.art_url or 'http://' in media.art_url: 129 | thumbnail = get_cached_filename(current_title) 130 | if not os.path.exists(thumbnail): 131 | print(f"Downloading thumbnail to cache: {thumbnail}") 132 | urllib.request.urlretrieve(media.art_url, thumbnail) 133 | else: 134 | print(f"Using cached thumbnail: {thumbnail}") 135 | height, width = 120, 120 136 | 137 | if thumbnail and os.path.exists(thumbnail): 138 | pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(thumbnail, height, width) 139 | circular_pixbuf = create_radius_pixbuf(pixbuf) 140 | 141 | print("Setting images safely...") 142 | safe_set_image(image, circular_pixbuf) 143 | safe_set_label(title_label, media_) 144 | image.show() 145 | 146 | 147 | except Exception as e: 148 | print(f"Exception in update_image: {e}") 149 | 150 | else: 151 | safe_set_label(title_label, '') 152 | image.hide() 153 | 154 | return True -------------------------------------------------------------------------------- /widgets.py: -------------------------------------------------------------------------------- 1 | import gi 2 | gi.require_version('Gtk', '3.0') 3 | 4 | from gi.repository import Gtk 5 | from date import RoundedCalendarLabel 6 | 7 | separator = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) 8 | separator.get_style_context().add_class('Separators') 9 | 10 | sys_separator = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) 11 | sys_separator.get_style_context().add_class('Separators') 12 | 13 | sys_separator_ = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) 14 | sys_separator_.get_style_context().add_class('Separators') 15 | 16 | media_separator = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) 17 | media_separator.get_style_context().add_class('Separators') 18 | 19 | timer_separator = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) 20 | timer_separator.get_style_context().add_class('Separators') 21 | 22 | 23 | search_entry = Gtk.SearchEntry() 24 | search_entry.get_style_context().add_class('App-Search') 25 | search_entry.set_placeholder_text('Search...') 26 | 27 | listbox = Gtk.ListBox() 28 | listbox.get_style_context().add_class('App-List') 29 | 30 | listbox.set_hexpand(False) 31 | listbox.set_vexpand(False) 32 | 33 | 34 | title_label = Gtk.Label() 35 | title_label.get_style_context().add_class("Media-Title") 36 | 37 | media_image = Gtk.Image() 38 | media_image.get_style_context().add_class("Media-Image") 39 | 40 | cpu_temp = Gtk.Label(label="CPU Temperature: Updating...") 41 | cpu_temp.get_style_context().add_class('sysInfo') 42 | 43 | cpu_usage = Gtk.Label(label="CPU Usage: Updating...") 44 | cpu_usage.get_style_context().add_class('sysInfo') 45 | 46 | ram_usage = Gtk.Label(label="RAM Usage: Updating...") 47 | ram_usage.get_style_context().add_class('sysInfo') 48 | 49 | used_ram = Gtk.Label(label="Used RAM: Updating...") 50 | used_ram.get_style_context().add_class('sysInfo') 51 | 52 | gpu_temp = Gtk.Label(label="GPU Temperature: Updating...") 53 | gpu_temp.get_style_context().add_class('sysInfo') 54 | 55 | gpu_usage = Gtk.Label(label="GPU Usage: Updating...") 56 | gpu_usage.get_style_context().add_class('sysInfo') 57 | 58 | gpu_vram = Gtk.Label(label="GPU VRAM: Updating...") 59 | gpu_vram.get_style_context().add_class('sysInfo') 60 | 61 | gpu_speed = Gtk.Label(label="GPU Speed: Updating...") 62 | gpu_speed.get_style_context().add_class('sysInfo') 63 | 64 | gpu_power = Gtk.Label(label="GPU Power: Updating...") 65 | gpu_power.get_style_context().add_class('sysInfo') 66 | 67 | 68 | time_label = Gtk.Label(label = 'Loading Time...') 69 | time_label.get_style_context().add_class('Clock') 70 | 71 | date_label = Gtk.Label(label = 'Loading Date...') 72 | date_label.get_style_context().add_class('Date') 73 | 74 | 75 | rounded_calendar = RoundedCalendarLabel() --------------------------------------------------------------------------------