├── .gitignore ├── README.md ├── app ├── __main__.py ├── icons │ ├── _dwindle.svg │ ├── _master.svg │ ├── background-app-sleepyface-symbolic.svg │ ├── bed-symbolic.svg │ ├── blur.svg │ ├── dwindle.svg │ ├── graph-symbolic.svg │ ├── key4-symbolic.svg │ ├── master.svg │ ├── move-to-window-symbolic.svg │ ├── overlapping-windows-symbolic.svg │ ├── password-entry-symbolic.svg │ ├── shapes-symbolic.svg │ ├── test-symbolic.svg │ └── window-symbolic.svg ├── modules │ ├── app.py │ ├── app_pages │ │ ├── __init__.py │ │ ├── animations.py │ │ ├── binds.py │ │ ├── decoration │ │ │ ├── __init__.py │ │ │ └── blur.py │ │ ├── general.py │ │ ├── gestures.py │ │ ├── group.py │ │ ├── idle.py │ │ ├── input.py │ │ ├── lock.py │ │ ├── misc.py │ │ ├── more.py │ │ ├── variables.py │ │ └── wallpaper.py │ ├── imports.py │ ├── utils.py │ └── widgets │ │ ├── BezierEditor.py │ │ ├── BezierEntryRow.py │ │ ├── ButtonRow.py │ │ ├── CheckButtonImage.py │ │ ├── ColorEntryRow.py │ │ ├── ColorExpanderRow.py │ │ ├── CustomToastOverlay.py │ │ ├── ExpanderRow.py │ │ ├── Icon.py │ │ ├── InfoButton.py │ │ ├── PreferencesGroup.py │ │ ├── SpinRow.py │ │ ├── SwitchRow.py │ │ └── __init__.py ├── style.css ├── style.css.map └── style.scss └── img └── app.png /.gitignore: -------------------------------------------------------------------------------- 1 | #/src/ 2 | **/__pycache__/ 3 | /build/ 4 | 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hyprset 2 | 3 | A GTK4/LibAdwaita tool to configure your Hyprland desktop. 4 | 5 | Built using [hyprparser-py](https://github.com/tokyob0t/hyprparser-py) 6 | 7 | ![app_image](./img/app.png) 8 | 9 | --- 10 | 11 | ##### Todo 12 | 13 | - [x] Add a smol popup to notify changes 14 | - [ ] Support colors, gradients, etc.. 15 | - [ ] Add a preview for decoration settings 16 | - [ ] Remove, add keybindings 17 | - [ ] Remove, add env vars 18 | - [ ] Remove, add startup cmds 19 | - [ ] Finish pages 20 | - [x] General 21 | - [X] Decoration 22 | - [ ] Animations 23 | - [ ] Input 24 | - [ ] Gestures 25 | - [ ] Group 26 | - [ ] Misc 27 | - [ ] Binds 28 | - [ ] Variables 29 | - [ ] More 30 | 31 | ##### Extra 32 | 33 | - [ ] Add pages for hyprpaper, hypridle, hyprlock... 34 | -------------------------------------------------------------------------------- /app/__main__.py: -------------------------------------------------------------------------------- 1 | from modules.app import MyApplication 2 | 3 | 4 | def main() -> None: 5 | try: 6 | MyApplication.run() 7 | except KeyboardInterrupt: 8 | pass 9 | except Exception as e: 10 | print(e) 11 | finally: 12 | exit(0) 13 | 14 | 15 | if __name__ == '__main__': 16 | main() 17 | -------------------------------------------------------------------------------- /app/icons/_dwindle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/icons/_master.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/icons/background-app-sleepyface-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/icons/bed-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/icons/blur.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | -------------------------------------------------------------------------------- /app/icons/dwindle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/icons/graph-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/icons/key4-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/icons/master.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/icons/move-to-window-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/icons/overlapping-windows-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/icons/password-entry-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/icons/shapes-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/icons/test-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/icons/window-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/modules/app.py: -------------------------------------------------------------------------------- 1 | from .app_pages import ( 2 | PAGES_DICT, 3 | PAGES_LIST, 4 | decoration_page, 5 | ) 6 | from .imports import Adw, Gdk, Gio, Gtk 7 | from .widgets import Icon, ToastOverlay, MyBezierEditorWindow 8 | 9 | 10 | class ApplicationWindow(Adw.ApplicationWindow): 11 | def __init__(self, app: Adw.Application): 12 | super().__init__(application=app) 13 | MyBezierEditorWindow.set_transient_for(self) 14 | 15 | self.root = Adw.OverlaySplitView.new() 16 | self.breakpoint = Adw.Breakpoint.new( 17 | Adw.BreakpointCondition.parse('max-width: 900px') # type: ignore 18 | ) 19 | self.set_size_request(700, 360) 20 | self.set_content(self.root) 21 | self.add_breakpoint(self.breakpoint) 22 | self.breakpoint.add_setter( 23 | self.root, 'collapsed', True # type: ignore 24 | ) 25 | 26 | # Main Content 27 | self.main_content = Adw.ToolbarView.new() 28 | self.main_content_navigation_page = Adw.NavigationPage.new( 29 | self.main_content, 'General' 30 | ) 31 | 32 | self.main_content_top_bar = Adw.HeaderBar.new() 33 | self.main_content_top_bar_title = Adw.WindowTitle.new( 34 | 'General', 'Gaps, borders, colors, cursor and other settings.' 35 | ) 36 | 37 | self.main_content.add_top_bar(self.main_content_top_bar) 38 | self.main_content_top_bar.set_title_widget( 39 | self.main_content_top_bar_title 40 | ) 41 | self.main_content_view_stack = Adw.ViewStack.new() 42 | 43 | self.toast_overlay = ToastOverlay 44 | self.toast_overlay.instance.set_child(self.main_content_view_stack) 45 | self.main_content.set_content(self.toast_overlay.instance) 46 | 47 | # Sidebar 48 | self.sidebar = Adw.ToolbarView() 49 | self.sidebar.add_css_class('list-box-scroll') 50 | self.sidebar_navigation_page = Adw.NavigationPage.new( 51 | self.sidebar, 'Settings' 52 | ) 53 | self.sidebar_navigation_page.add_css_class('sidebar') 54 | self.sidebar_top_bar = Adw.HeaderBar.new() 55 | self.sidebar.add_top_bar(self.sidebar_top_bar) 56 | self.sidebar_scrolled_window = Gtk.ScrolledWindow.new() 57 | self.sidebar_listbox = Gtk.ListBox.new() 58 | self.sidebar_scrolled_window.set_child(self.sidebar_listbox) 59 | self.sidebar.set_content(self.sidebar_scrolled_window) 60 | 61 | # Sidebar Stuff 62 | for item in PAGES_LIST: 63 | if item.get('separator'): 64 | tmp_rowbox = Gtk.ListBoxRow.new() 65 | tmp_rowbox.set_can_focus(False) 66 | tmp_rowbox.set_activatable(False) 67 | tmp_rowbox.set_selectable(False) 68 | tmp_rowbox.set_sensitive(False) 69 | tmp_rowbox.set_child( 70 | Gtk.Separator.new(Gtk.Orientation.HORIZONTAL) 71 | ) 72 | 73 | else: 74 | tmp_grid = Gtk.Grid.new() 75 | tmp_grid.set_column_spacing(12) 76 | tmp_grid.set_valign(Gtk.Align.CENTER) 77 | tmp_grid.set_vexpand(True) 78 | 79 | tmp_rowbox = Gtk.ListBoxRow.new() 80 | tmp_rowbox.add_css_class('list-box-row') 81 | setattr(tmp_rowbox, 'title', item['label']) 82 | setattr(tmp_rowbox, 'desc', item['desc']) 83 | 84 | label = Gtk.Label.new(item['label']) 85 | tmp_grid.attach(Icon(item['icon']), 0, 0, 1, 1) 86 | tmp_grid.attach(label, 1, 0, 1, 1) 87 | tmp_rowbox.set_child(tmp_grid) 88 | 89 | self.sidebar_listbox.append(tmp_rowbox) 90 | 91 | self.root.set_content(self.main_content_navigation_page) 92 | self.root.set_sidebar(self.sidebar_navigation_page) 93 | 94 | self.sidebar_listbox.connect('row-activated', self.on_row_activated) 95 | 96 | shortcut_controller = Gtk.ShortcutController.new() 97 | # Add ctrl+s shortcut 98 | shortcut_controller.add_shortcut( 99 | Gtk.Shortcut.new( 100 | Gtk.ShortcutTrigger.parse_string('s'), 101 | Gtk.CallbackAction.new(self.toast_overlay.save_changes), 102 | ) 103 | ) 104 | 105 | self.root.add_controller(shortcut_controller) 106 | self.present() 107 | 108 | self.sidebar_listbox.unselect_all() 109 | return self.add_pages() 110 | 111 | def on_row_activated(self, _, sidebar_rowbox: Gtk.ListBoxRow): 112 | 113 | match self.main_content_view_stack.get_visible_child_name().lower(): 114 | case 'general': 115 | pass 116 | case 'decoration': 117 | decoration_page.pop_to_tag('index-page') 118 | case _: 119 | pass 120 | self.main_content_top_bar_title.set_title( 121 | getattr(sidebar_rowbox, 'title') 122 | ) 123 | self.main_content_top_bar_title.set_subtitle( 124 | getattr(sidebar_rowbox, 'desc') 125 | ) 126 | self.main_content_view_stack.set_visible_child_name( 127 | getattr(sidebar_rowbox, 'title') 128 | ) 129 | 130 | def add_pages(self): 131 | for name, page in PAGES_DICT.items(): 132 | self.main_content_view_stack.add_named( 133 | page, 134 | name, 135 | ) 136 | 137 | 138 | class Application(Adw.Application): 139 | def __init__(self) -> None: 140 | super().__init__() 141 | self.window = None 142 | self.set_application_id('com.tokyob0t.HyprSettings') 143 | self.set_flags(Gio.ApplicationFlags.FLAGS_NONE) 144 | self.load_css() 145 | 146 | def load_css(self) -> None: 147 | css_provider = Gtk.CssProvider() 148 | css_provider.load_from_path(f'{__file__[:-15]}/style.css') 149 | 150 | return Gtk.StyleContext.add_provider_for_display( # type: ignore 151 | Gdk.Display.get_default(), 152 | css_provider, 153 | Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION, 154 | ) 155 | 156 | def do_activate(self) -> None: 157 | if not self.window: 158 | self.window = ApplicationWindow(self) 159 | return self.window.present() 160 | 161 | 162 | MyApplication = Application() 163 | -------------------------------------------------------------------------------- /app/modules/app_pages/__init__.py: -------------------------------------------------------------------------------- 1 | from .animations import animations_page 2 | from .decoration import decoration_page 3 | from .general import general_page 4 | 5 | PAGES_DICT = { 6 | 'General': general_page, 7 | 'Decoration': decoration_page, 8 | 'Animations': animations_page, 9 | } 10 | 11 | PAGES_LIST = [ 12 | { 13 | 'icon': 'settings-symbolic', 14 | 'label': 'General', 15 | 'desc': 'Gaps, borders, colors, cursor and other settings.', 16 | }, 17 | { 18 | 'icon': 'window-new-symbolic', 19 | 'label': 'Decoration', 20 | 'desc': 'Rounding, blur, transparency, shadow and dim settings.', 21 | }, 22 | { 23 | 'icon': 'move-to-window-symbolic', 24 | 'label': 'Animations', 25 | 'desc': 'Change animations settings.', 26 | }, 27 | {'separator': True}, 28 | { 29 | 'icon': 'input-keyboard-symbolic', 30 | 'label': 'Input', 31 | 'desc': 'Change input settings.', 32 | }, 33 | { 34 | 'icon': 'input-touchpad-symbolic', 35 | 'label': 'Gestures', 36 | 'desc': 'Gesture and swipe settings.', 37 | }, 38 | { 39 | 'icon': 'overlapping-windows-symbolic', 40 | 'label': 'Group', 41 | 'desc': 'Change group settings.', 42 | }, 43 | {'separator': True}, 44 | { 45 | 'icon': 'preferences-system-symbolic', 46 | 'label': 'Misc', 47 | 'desc': 'Change miscellaneous settings.', 48 | }, 49 | { 50 | 'icon': 'preferences-desktop-keyboard-shortcuts-symbolic', 51 | 'label': 'Binds', 52 | 'desc': 'Change binds settings.', 53 | }, 54 | { 55 | 'icon': 'test-symbolic', 56 | 'label': 'Variables', 57 | 'desc': 'Adjust variables.', 58 | }, 59 | {'separator': True}, 60 | { 61 | # "icon": "preferences-desktop-wallpaper-symbolic", 62 | 'icon': 'preferences-desktop-appearance-symbolic', 63 | 'label': 'Wallpaper', 64 | 'desc': 'Hyprpaper settings.', 65 | }, 66 | { 67 | 'icon': 'background-app-sleepyface-symbolic', 68 | 'label': 'Idle', 69 | 'desc': 'Hypridle settings.', 70 | }, 71 | { 72 | 'icon': 'key4-symbolic', 73 | 'label': 'Lock', 74 | 'desc': 'Hyprlock settings.', 75 | }, 76 | {'separator': True}, 77 | {'icon': 'view-more-symbolic', 'label': 'More', 'desc': ''}, 78 | ] 79 | -------------------------------------------------------------------------------- /app/modules/app_pages/animations.py: -------------------------------------------------------------------------------- 1 | from ..widgets import ( 2 | PreferencesGroup, 3 | SwitchRow, 4 | BezierGroup, 5 | InfoButton, 6 | ExpanderRow, 7 | ) 8 | from ..imports import Adw 9 | 10 | 11 | animations_page = Adw.PreferencesPage.new() 12 | 13 | 14 | settings_animations = PreferencesGroup("", "") 15 | settings_animations.add( 16 | SwitchRow( 17 | "Animations Enabled", 18 | "Enable animations.", 19 | "animations:enabled", 20 | ) 21 | ) 22 | 23 | settings_animations.add( 24 | SwitchRow( 25 | "First Launch Animation", 26 | "Enable first launch animation.", 27 | "animations:first_launch_animation", 28 | ) 29 | ) 30 | 31 | settings_bezier = BezierGroup() 32 | settings_anim_tree = PreferencesGroup( 33 | "Animation Tree", 34 | "Animation tree for windows, layers, border and workspaces.", 35 | ) 36 | settings_anim_tree_windows = ExpanderRow("Windows", "") 37 | settings_anim_tree_windows_windowsIn = ExpanderRow("Windows In", "Window open") 38 | settings_anim_tree_windows_windowsOut = ExpanderRow("Windows Out", "Window close") 39 | settings_anim_tree_windows_windowsMove = ExpanderRow( 40 | "Windows In", "Everything in between, moving, dragging and resizing." 41 | ) 42 | 43 | 44 | settings_anim_tree.set_header_suffix( 45 | InfoButton( 46 | "The animations are a tree. If an animation is unset, it will inherit its parent’s values." 47 | ) 48 | ) 49 | 50 | for i in [ 51 | settings_anim_tree_windows_windowsIn, 52 | settings_anim_tree_windows_windowsOut, 53 | settings_anim_tree_windows_windowsMove, 54 | ]: 55 | settings_anim_tree_windows.add_row(i) 56 | 57 | for i in [ 58 | settings_anim_tree_windows, 59 | ]: 60 | settings_anim_tree.add(i) 61 | 62 | 63 | for i in [settings_animations, settings_bezier, settings_anim_tree]: 64 | animations_page.add(i) 65 | -------------------------------------------------------------------------------- /app/modules/app_pages/binds.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyprland-community/hyprset/3b5a875058e753193deb806c9739dbea18e2f897/app/modules/app_pages/binds.py -------------------------------------------------------------------------------- /app/modules/app_pages/decoration/__init__.py: -------------------------------------------------------------------------------- 1 | from ...widgets import ( 2 | ButtonRow, 3 | ColorEntryRow, 4 | PreferencesGroup, 5 | SpinRow, 6 | SwitchRow, 7 | ) 8 | from .blur import blur_page 9 | from ...imports import Adw 10 | 11 | 12 | index_page = Adw.NavigationPage( 13 | child=Adw.PreferencesPage.new(), title="TEEEEST", tag="index-page" 14 | ) 15 | 16 | 17 | index_page_content: Adw.PreferencesPage = index_page.get_child() # type:ignore 18 | 19 | decoration_page = Adw.NavigationView.new() 20 | decoration_page.add(index_page) 21 | decoration_page.add(blur_page) 22 | 23 | # NavigationView -> NavigationPage -> PreferencesPage -> PreferencesGroup 24 | # index_page = Adw.NavigationPage.new() 25 | 26 | settings_rounding = PreferencesGroup("", "") 27 | 28 | settings_rounding_spinrow = SpinRow( 29 | "Rounding", 30 | "Rounded corners' radius (in layout px).", 31 | "decoration:rounding", 32 | ) 33 | settings_rounding.add(settings_rounding_spinrow) 34 | 35 | settings_opacity = PreferencesGroup( 36 | "Opacity", "Active, inactive and fullscreen opacity." 37 | ) 38 | settings_opacity_active = SpinRow( 39 | "Active Opacity", 40 | "Opacity of active windows.", 41 | "decoration:active_opacity", 42 | data_type=float, 43 | max=1.0, 44 | ) 45 | 46 | settings_opacity_inactive = SpinRow( 47 | "Inactive Opacity", 48 | "Opacity of inactive windows.", 49 | "decoration:inactive_opacity", 50 | data_type=float, 51 | max=1.0, 52 | ) 53 | settings_opacity_fullscreen = SpinRow( 54 | "Fullscreen Opacity", 55 | "Opacity of fullscreen windows.", 56 | "decoration:fullscreen_opacity", 57 | data_type=float, 58 | max=1.0, 59 | ) 60 | 61 | settings_shadow = PreferencesGroup("Shadow", "Drop shadow, range, power and colors.") 62 | settings_shadow_drop_shadow = SwitchRow( 63 | "Drop Shadow", "Enable drop shadows on windows.", "decoration:drop_shadow" 64 | ) 65 | settings_shadow_range = SpinRow( 66 | "Shadow Range", 67 | "Shadow range (“size”) in layout px.", 68 | "decoration:shadow_range", 69 | ) 70 | settings_shadow_render_power = SpinRow( 71 | "Shadow Render Power", 72 | "In what power to render the falloff (more power, the faster the falloff).", 73 | "decoration:shadow_render_power", 74 | min=1, 75 | max=4, 76 | ) 77 | 78 | 79 | settings_shadow_ignore_window = SwitchRow( 80 | "Shadow Ignore Window", 81 | "If enabled, the shadow will not be rendered behind the window itself, only around it.", 82 | "decoration:shadow_ignore_window", 83 | ) 84 | settings_shadow_color = ColorEntryRow( 85 | "Shadow's Color", 86 | "Shadow's color. Alpha dictates shadow’s opacity.", 87 | "decoration:col.shadow", 88 | ) 89 | settings_shadow_color_inactive = ColorEntryRow( 90 | "Inactive Shadow Color", 91 | "Inactive shadow color. If not set, will fall back to col.shadow.", 92 | "decoration:col.shadow_inactive", 93 | ) 94 | settings_shadow_scale = SpinRow( 95 | "Shadow's Scale", 96 | "Shadow's scale.", 97 | "decoration:shadow_scale", 98 | max=1.0, 99 | data_type=float, 100 | ) 101 | settings_dim = PreferencesGroup("Dim", "Change dim settings.") 102 | settings_dim_inactive_window = SwitchRow( 103 | "Inactive Window", 104 | "Enables dimming of inactive windows.", 105 | "decoration:dim_inactive", 106 | ) 107 | settings_dim_strenght = SpinRow( 108 | "Dim Strenght", 109 | "How much inactive windows should be dimmed.", 110 | "decoration:dim_strength", 111 | data_type=float, 112 | max=1.0, 113 | ) 114 | settings_dim_special = SpinRow( 115 | "Dim Special", 116 | "How much to dim the rest of the screen by when a special workspace is open.", 117 | "decoration:dim_special", 118 | data_type=float, 119 | max=1.0, 120 | ) 121 | settings_dim_around = SpinRow( 122 | "Dim Around", 123 | "How much the dimaround window rule should dim by.", 124 | "decoration:dim_around", 125 | data_type=float, 126 | max=1.0, 127 | ) 128 | 129 | for i in [ 130 | settings_dim_inactive_window, 131 | settings_dim_strenght, 132 | settings_dim_special, 133 | settings_dim_around, 134 | ]: 135 | settings_dim.add(i) 136 | 137 | 138 | for i in [ 139 | settings_shadow_drop_shadow, 140 | settings_shadow_range, 141 | settings_shadow_render_power, 142 | settings_shadow_ignore_window, 143 | settings_shadow_color, 144 | settings_shadow_color_inactive, 145 | settings_shadow_scale, 146 | ]: 147 | settings_shadow.add(i) 148 | 149 | for i in [ 150 | settings_opacity_active, 151 | settings_opacity_inactive, 152 | settings_opacity_fullscreen, 153 | ]: 154 | settings_opacity.add(i) 155 | 156 | 157 | for i in [settings_rounding, settings_opacity, settings_shadow, settings_dim]: 158 | index_page_content.add(i) 159 | 160 | 161 | settings_blur = PreferencesGroup("", "") 162 | settings_blur.add( 163 | ButtonRow( 164 | "tool-gradient-conical-symbolic", 165 | "Blur", 166 | "Size, passes, noise, contrast, vibrancy...", 167 | lambda *_: decoration_page.push_by_tag("blur-page"), 168 | ) 169 | ) 170 | index_page_content.add(settings_blur) 171 | -------------------------------------------------------------------------------- /app/modules/app_pages/decoration/blur.py: -------------------------------------------------------------------------------- 1 | from ...widgets import PreferencesGroup, SpinRow, SwitchRow 2 | from ...imports import Adw 3 | 4 | 5 | blur_page = Adw.NavigationPage.new(Adw.PreferencesPage.new(), title="TEEEEST") 6 | blur_page.set_tag("blur-page") 7 | blur_page_content: Adw.PreferencesPage = blur_page.get_child() # type: ignore 8 | 9 | 10 | settings_blur = PreferencesGroup("", "") 11 | 12 | settings_blur_size = SpinRow( 13 | "Blur Size", "Blur size (distance).", "decoration:blur:size", min=1 14 | ) 15 | settings_blur_passes = SpinRow( 16 | "Blur Passes", 17 | "The amount of passes to perform.", 18 | "decoration:blur:passes", 19 | min=1, 20 | ) 21 | 22 | settings_blur_ignore_opacity = SwitchRow( 23 | "Ignore Opacity", 24 | "Make the blur layer ignore the opacity of the window.", 25 | "decoration:blur:ignore_opacity", 26 | ) 27 | settings_blur_new_optimizations = SwitchRow( 28 | "New Optimizations", 29 | "Whether to enable further optimizations to the blur. Recommended to leave on, as it will massively improve performance.", 30 | "decoration:blur:new_optimizations", 31 | ) 32 | settings_blur_xray = SwitchRow( 33 | "Blur Xray", 34 | "If enabled, floating windows will ignore tiled windows in their blur. Only available if blur_new_optimizations is true. Will reduce overhead on floating blur significantly.", 35 | "decoration:blur:xray", 36 | ) 37 | 38 | settings_blur_noise = SpinRow( 39 | "Blur Noise", 40 | "How much noise to apply.", 41 | "decoration:blur:noise", 42 | data_type=float, 43 | max=1, 44 | decimal_digits=4, 45 | ) 46 | settings_blur_contrast = SpinRow( 47 | "Blur Contrast", 48 | "Contrast modulation for blur.", 49 | "decoration:blur:contrast", 50 | data_type=float, 51 | max=2, 52 | decimal_digits=4, 53 | ) 54 | settings_blur_brightness = SpinRow( 55 | "Blur Brightness", 56 | "Brightness modulation for blur.", 57 | "decoration:blur:brightness", 58 | data_type=float, 59 | max=2, 60 | decimal_digits=4, 61 | ) 62 | settings_blur_vibrancy = SpinRow( 63 | "Vibrancy", 64 | "Increase saturation of blurred colors.", 65 | "decoration:blur:vibrancy", 66 | data_type=float, 67 | max=1, 68 | decimal_digits=4, 69 | ) 70 | settings_blur_vibrancy_darkness = SpinRow( 71 | "Vibrancy Darkness", 72 | "How strong the effect of vibrancy is on dark areas.", 73 | "decoration:blur:vibrancy_darkness", 74 | data_type=float, 75 | max=1, 76 | decimal_digits=4, 77 | ) 78 | settings_blur_special = SwitchRow( 79 | "Blur Special", 80 | "Whether to blur behind the special workspace (note: expensive).", 81 | "decoration:blur:special", 82 | ) 83 | settings_blur_popups = SwitchRow( 84 | "Blur Popups", 85 | "Whether to blur popups (e.g. right-click menus).", 86 | "decoration:blur:popups", 87 | ) 88 | settings_blur_popups_ignorealpha = SpinRow( 89 | "Popups Ignore Alpha", 90 | "Works like ignorealpha in layer rules. If pixel opacity is below set value, will not blur.", 91 | "decoration:blur:popups_ignorealpha", 92 | data_type=float, 93 | max=1, 94 | ) 95 | 96 | 97 | for i in [ 98 | settings_blur_size, 99 | settings_blur_passes, 100 | settings_blur_ignore_opacity, 101 | settings_blur_new_optimizations, 102 | settings_blur_xray, 103 | settings_blur_noise, 104 | settings_blur_contrast, 105 | settings_blur_brightness, 106 | settings_blur_vibrancy, 107 | settings_blur_vibrancy_darkness, 108 | settings_blur_special, 109 | settings_blur_popups, 110 | settings_blur_popups_ignorealpha, 111 | ]: 112 | if hasattr(i, "instance"): 113 | settings_blur.add(i.instance) 114 | else: 115 | settings_blur.add(i) 116 | 117 | settings_blur_enabled = PreferencesGroup( 118 | "", 119 | "", 120 | ) 121 | settings_blur_enabled.add( 122 | SwitchRow( 123 | "Blur Enabled", 124 | "Enable kawase window background blur.", 125 | "decoration:blur:enabled", 126 | ) 127 | ) 128 | 129 | 130 | blur_page_content.add(settings_blur_enabled) 131 | blur_page_content.add(settings_blur) 132 | -------------------------------------------------------------------------------- /app/modules/app_pages/general.py: -------------------------------------------------------------------------------- 1 | from ..imports import Setting, Adw, Gtk, HyprData 2 | 3 | from ..widgets import ( 4 | CheckButtonImage, 5 | ColorExpanderRow, 6 | InfoButton, 7 | PreferencesGroup, 8 | SpinRow, 9 | SwitchRow, 10 | ) 11 | 12 | general_page = Adw.PreferencesPage.new() 13 | 14 | # Gaps 15 | settings_gaps = PreferencesGroup( 16 | "Gaps", "Change gaps in/out and gaps between workspaces." 17 | ) 18 | 19 | 20 | settings_gaps_in = SpinRow("Gaps In", "Gaps between windows.", "general:gaps_in") 21 | settings_gaps_out = SpinRow( 22 | "Gaps Out", "Gaps between windows and monitor edges.", "general:gaps_out" 23 | ) 24 | settings_gaps_workspaces = SpinRow( 25 | "Gaps Workspaces", 26 | "Gaps between workspaces. Stacks with gaps_out.", 27 | "general:gaps_workspaces", 28 | ) 29 | 30 | # Borders 31 | settings_borders = PreferencesGroup("Borders", "Size, resize, floating...") 32 | 33 | settings_borders_border_size = SpinRow( 34 | "Border Size", "Size of the border around windows.", "general:border_size" 35 | ) 36 | 37 | settings_borders_noborder_onfloating = SwitchRow( 38 | "Border on Floating", 39 | "Enable borders for floating windows.", 40 | "general:no_border_on_floating", 41 | invert=True, 42 | ) 43 | 44 | settings_borders_resize_onborder = SwitchRow( 45 | "Resize on Border", 46 | "Enables resizing windows by clicking and dragging on borders and gaps.", 47 | "general:resize_on_border", 48 | ) 49 | 50 | settings_borders_extend_border = SpinRow( 51 | "Extend Border Grab Area", 52 | "Extends the area around the border where you can click and drag on, only used when general:resize_on_border is on.", 53 | "general:extend_border_grab_area", 54 | ) 55 | 56 | settings_borders_hover_icon_onborder = SwitchRow( 57 | "Hover Icon on Border", 58 | "Show a cursor icon when hovering over borders, only used when general:resize_on_border is on.", 59 | "general:hover_icon_on_border", 60 | ) 61 | 62 | 63 | # Colors 64 | 65 | settings_colors = PreferencesGroup("Colors", "Change borders colors.") 66 | 67 | settings_colors_inactive_border = ColorExpanderRow( 68 | "Inactive Border Color", 69 | "Border color for inactive windows.", 70 | "general:col.inactive_border", 71 | ) 72 | 73 | settings_colors_active_border = ColorExpanderRow( 74 | "Active Border Color", 75 | "Border color for active windows.", 76 | "general:col.active_border", 77 | ) 78 | 79 | settings_colors_nogroup_border = ColorExpanderRow( 80 | "No Group Border Color", 81 | "Inactive border color for window that cannot be added to a group.", 82 | "general:col.nogroup_border", 83 | ) 84 | 85 | settings_colors_nogroup_active_border = ColorExpanderRow( 86 | "No Group Active Border Color", 87 | "Active border color for window that cannot be added to a group.", 88 | "general:col.nogroup_border_active", 89 | ) 90 | 91 | # Cursor 92 | settings_cursor = PreferencesGroup("Cursor", "Change cursor settings.") 93 | 94 | settings_cursor_no_focus_fallback = SwitchRow( 95 | "No Focus Fallback", 96 | "If enabled, will not fall back to the next available window when moving focus in a direction where no window was found.", 97 | "general:no_focus_fallback", 98 | ) 99 | 100 | # Other 101 | settings_other = PreferencesGroup("", "") 102 | 103 | # Layout Chooser Row 104 | settings_other_layout = Adw.ActionRow.new() 105 | 106 | # Vertical container 107 | settings_other_layout_container_v = Gtk.Box( 108 | orientation=Gtk.Orientation.VERTICAL, 109 | css_classes=["title", "vertical"], 110 | margin_end=12, 111 | margin_start=12, 112 | margin_top=6, 113 | margin_bottom=6, 114 | ) 115 | 116 | 117 | # Title 118 | settings_other_layout_container_v_title = Gtk.Label( 119 | label="Layout", css_classes=["title"], halign=Gtk.Align.START 120 | ) 121 | # Subtitle 122 | settings_other_layout_container_v_subtitle = Gtk.Label( 123 | label="Which layout to use.", css_classes=["subtitle"], halign=Gtk.Align.START 124 | ) 125 | 126 | # Append title & subtitle 127 | settings_other_layout_container_v.append(settings_other_layout_container_v_title) 128 | settings_other_layout_container_v.append(settings_other_layout_container_v_subtitle) 129 | 130 | settings_other_layout_container_h = Gtk.Box( 131 | orientation=Gtk.Orientation.HORIZONTAL, spacing=24, homogeneous=True 132 | ) 133 | 134 | # Checkbuttons 135 | settings_other_layout_checkbutton_dwindle = CheckButtonImage("Dwindle", "dwindle") 136 | 137 | settings_other_layout_checkbutton_master = CheckButtonImage("Master", "master") 138 | settings_other_layout_checkbutton_master.checkbutton.set_group( 139 | settings_other_layout_checkbutton_dwindle.checkbutton 140 | ) 141 | 142 | 143 | default = HyprData.get_option("general:layout") 144 | 145 | if not default: 146 | HyprData.new_option(Setting("general:layout", "dwindle")) 147 | default = "dwindle" # type: ignore 148 | else: 149 | default = default.value 150 | 151 | if default == "master": 152 | settings_other_layout_checkbutton_master.checkbutton.set_active(True) 153 | else: 154 | settings_other_layout_checkbutton_dwindle.checkbutton.set_active(True) 155 | 156 | 157 | settings_other_layout_container_h.append(settings_other_layout_checkbutton_dwindle) 158 | settings_other_layout_container_h.append(settings_other_layout_checkbutton_master) 159 | 160 | # Append hbox to vbox 161 | settings_other_layout_container_v.append(settings_other_layout_container_h) 162 | 163 | settings_other_layout.set_child(settings_other_layout_container_v) 164 | 165 | # 166 | settings_other_allow_tearing = SwitchRow( 167 | "Allow Tearing", 168 | "Master switch for allowing tearing to occur. See the Tearing Page.", 169 | "general:allow_tearing", 170 | ) 171 | 172 | settings_other.add(settings_other_layout) 173 | settings_other.add(settings_other_allow_tearing) 174 | 175 | 176 | # Add Cursor settings 177 | for i in [ 178 | settings_cursor_no_focus_fallback, 179 | ]: 180 | settings_cursor.add(i) 181 | 182 | 183 | # Add Gaps settings 184 | for i in [settings_gaps_in, settings_gaps_out, settings_gaps_workspaces]: 185 | settings_gaps.add(i) 186 | 187 | 188 | # Add Border settings 189 | for i in [ 190 | settings_borders_border_size, 191 | settings_borders_noborder_onfloating, 192 | settings_borders_resize_onborder, 193 | settings_borders_extend_border, 194 | settings_borders_hover_icon_onborder, 195 | ]: 196 | settings_borders.add(i) 197 | 198 | 199 | # Add Color settings 200 | for i in [ 201 | settings_colors_inactive_border, 202 | settings_colors_active_border, 203 | settings_colors_nogroup_border, 204 | settings_colors_nogroup_active_border, 205 | ]: 206 | settings_colors.add(i) 207 | 208 | 209 | # Add sections 210 | for i in [ 211 | settings_gaps, 212 | settings_borders, 213 | settings_colors, 214 | settings_cursor, 215 | settings_other, 216 | ]: 217 | general_page.add(i) 218 | -------------------------------------------------------------------------------- /app/modules/app_pages/gestures.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyprland-community/hyprset/3b5a875058e753193deb806c9739dbea18e2f897/app/modules/app_pages/gestures.py -------------------------------------------------------------------------------- /app/modules/app_pages/group.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyprland-community/hyprset/3b5a875058e753193deb806c9739dbea18e2f897/app/modules/app_pages/group.py -------------------------------------------------------------------------------- /app/modules/app_pages/idle.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyprland-community/hyprset/3b5a875058e753193deb806c9739dbea18e2f897/app/modules/app_pages/idle.py -------------------------------------------------------------------------------- /app/modules/app_pages/input.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyprland-community/hyprset/3b5a875058e753193deb806c9739dbea18e2f897/app/modules/app_pages/input.py -------------------------------------------------------------------------------- /app/modules/app_pages/lock.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyprland-community/hyprset/3b5a875058e753193deb806c9739dbea18e2f897/app/modules/app_pages/lock.py -------------------------------------------------------------------------------- /app/modules/app_pages/misc.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyprland-community/hyprset/3b5a875058e753193deb806c9739dbea18e2f897/app/modules/app_pages/misc.py -------------------------------------------------------------------------------- /app/modules/app_pages/more.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyprland-community/hyprset/3b5a875058e753193deb806c9739dbea18e2f897/app/modules/app_pages/more.py -------------------------------------------------------------------------------- /app/modules/app_pages/variables.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyprland-community/hyprset/3b5a875058e753193deb806c9739dbea18e2f897/app/modules/app_pages/variables.py -------------------------------------------------------------------------------- /app/modules/app_pages/wallpaper.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyprland-community/hyprset/3b5a875058e753193deb806c9739dbea18e2f897/app/modules/app_pages/wallpaper.py -------------------------------------------------------------------------------- /app/modules/imports.py: -------------------------------------------------------------------------------- 1 | # ruff: noqa 2 | from typing import List, Literal, Type, Union, Tuple, Optional 3 | 4 | 5 | import gi 6 | import re 7 | import string 8 | 9 | gi.require_versions({"Adw": "1", "GdkPixbuf": "2.0", "Gdk": "4.0", "Gtk": "4.0"}) 10 | from gi.repository import Adw, Gdk, GdkPixbuf, Gio, GLib, Gtk, cairo, GObject 11 | from hyprparser import Bezier, Color, Gradient, HyprData, Setting 12 | 13 | Gtk.Settings.get_default().set_property("gtk-icon-theme-name", "Adwaita") # type: ignore 14 | 15 | # Gtk.IconTheme.get_for_display(Gdk.Display.get_default()).add_search_path( 16 | # __file__[:-19] + "/icons" 17 | # ) 18 | -------------------------------------------------------------------------------- /app/modules/utils.py: -------------------------------------------------------------------------------- 1 | from .imports import Gdk, Literal, Tuple, Gtk 2 | import string 3 | 4 | 5 | string.ascii_lowercase = string.ascii_lowercase + ' ' 6 | 7 | # Im so fukin dumb, didnt know that Gdk.RGBA had Gdk.RGBA.parse() function 8 | 9 | 10 | class ParseColor: 11 | @staticmethod 12 | def rgba_str_to_hex(color: str) -> str: 13 | color = ( 14 | ParseColor.format_rgba(color).replace('rgba(', '').replace(')', '') 15 | ) 16 | rgba = list(map(int, color.split(','))) 17 | 18 | r, g, b = rgba[:3] 19 | a = rgba[3] if len(rgba) == 4 else 255 20 | 21 | return f'#{r:02X}{g:02X}{b:02X}{a:02X}' 22 | 23 | @staticmethod 24 | def rgba_float_to_hex(color: Tuple[float, float, float, float]) -> str: 25 | r = int(color[0] * 255.0) 26 | g = int(color[1] * 255.0) 27 | b = int(color[2] * 255.0) 28 | a = int(color[3] * 255.0) 29 | 30 | return f'#{r:02X}{g:02X}{b:02X}{a:02X}' 31 | 32 | @staticmethod 33 | def hex_to_rgba_float(color: str) -> Tuple[float, float, float, float]: 34 | color = ParseColor.format_hex(color) 35 | r = int(color[1:3], 16) / 255.0 36 | g = int(color[3:5], 16) / 255.0 37 | b = int(color[5:7], 16) / 255.0 38 | a = (int(color[7:9], 16) / 255.0) if len(color) == 8 else 1.0 39 | return (r, g, b, a) 40 | 41 | @staticmethod 42 | def hex_to_rgba_str(color: str) -> str: 43 | color = ParseColor.format_hex(color) 44 | 45 | r = int(color[1:3], 16) 46 | g = int(color[3:5], 16) 47 | b = int(color[5:7], 16) 48 | a = int(color[7:9], 16) / 255 49 | 50 | return f'rgba({r},{g},{b},{a:.2f})' 51 | 52 | @staticmethod 53 | def hex_to_gdk_rgba(color: str) -> Gdk.RGBA: 54 | color = ParseColor.format_hex(color) 55 | r = int(color[1:3], 16) / 255.0 56 | g = int(color[3:5], 16) / 255.0 57 | b = int(color[5:7], 16) / 255.0 58 | a = int(color[7:9], 16) / 255.0 if len(color) == 9 else 1.0 59 | return Gdk.RGBA(r, g, b, a) # type:ignore 60 | 61 | @staticmethod 62 | def gdk_rgba_to_hex(color: Gdk.RGBA) -> str: 63 | r = int(color.red * 255) # type: ignore 64 | g = int(color.green * 255) # type:ignore 65 | b = int(color.blue * 255) # type:ignore 66 | a = int(color.alpha * 255) # type:ignore 67 | return f'#{r:02X}{g:02X}{b:02X}{a:02X}' 68 | 69 | @staticmethod 70 | def format_hex(text: str) -> str: 71 | color = text.strip().lower().replace('#', '') 72 | color = ''.join(i if i in '1234567890abcdef' else 'f' for i in color) 73 | 74 | if len(text) < 6: 75 | color = f'{color:0<6}' 76 | 77 | if len(text) < 8: 78 | color = f'{color:f<8}' 79 | 80 | if len(text) > 8: 81 | color = color[:8] 82 | 83 | return '#{}'.format(color) 84 | 85 | @staticmethod 86 | def format_rgba(text: str) -> str: 87 | color = ( 88 | text.strip() 89 | .lower() 90 | .replace('rgba', '') 91 | .replace('rgb', '') 92 | .strip('()') 93 | ) 94 | 95 | color = ''.join( 96 | i if i in '1234567890,' else '0' 97 | for i in color 98 | if i not in string.ascii_lowercase 99 | ) 100 | 101 | sections = color.split(',') 102 | sections = [s.ljust(3, '0')[:3] for s in sections] 103 | 104 | if len(sections) < 3: 105 | sections.extend(['0'] * (3 - len(sections))) 106 | 107 | if len(sections) == 3: 108 | if text.strip().startswith('rgba'): 109 | sections.append('0') 110 | elif text.strip().startswith('rgb'): 111 | sections.append('255') 112 | 113 | return 'rgba({})'.format(','.join(sections)) 114 | 115 | @staticmethod 116 | def is_color(text: str) -> bool: 117 | text = text.strip().lower() 118 | if text.startswith('rgb'): 119 | return True 120 | elif text.startswith('#'): 121 | return True 122 | return False 123 | 124 | @staticmethod 125 | def color_type(text: str) -> Literal['rgba', 'hex', None]: 126 | text = text.strip().lower() 127 | if text.startswith('rgb'): 128 | return 'rgba' 129 | elif text.startswith('#'): 130 | return 'hex' 131 | return None 132 | 133 | 134 | # idk how else obtain a gtk theme var, so 135 | 136 | tmp = Gtk.Box() 137 | tmp.add_css_class('custom-box') 138 | 139 | ctx = tmp.get_style_context() 140 | 141 | provider = Gtk.CssProvider.new() 142 | 143 | provider.load_from_data('.custom-box {color: @accent_color; }') 144 | ctx.add_provider(provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) 145 | accent_color: Gdk.RGBA = ctx.get_color() # type: ignore 146 | 147 | provider.load_from_data('.custom-box {color: @card_bg_color; }') 148 | ctx.add_provider(provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) 149 | bg_color: Gdk.RGBA = ctx.get_color() # type: ignore 150 | 151 | provider.load_from_data('.custom-box {color: @card_fg_color; }') 152 | ctx.add_provider(provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) 153 | fg_color: Gdk.RGBA = ctx.get_color() # type: ignore 154 | -------------------------------------------------------------------------------- /app/modules/widgets/BezierEditor.py: -------------------------------------------------------------------------------- 1 | from ..imports import Gtk, Gdk, Adw, GObject, Bezier, Tuple, Union 2 | from ..utils import fg_color, accent_color 3 | from dataclasses import dataclass 4 | import math 5 | 6 | 7 | @dataclass 8 | class Point: 9 | x: Union[int, float] 10 | y: Union[int, float] 11 | 12 | 13 | class BezierEditor(Gtk.DrawingArea): 14 | __gsignals__ = { 15 | 'changed': (GObject.SignalFlags.RUN_FIRST, None, ()), 16 | } 17 | 18 | def __init__(self): 19 | super().__init__() 20 | self.add_css_class('bezier-editor') 21 | self.set_size_request(400, 400) 22 | self.set_halign(Gtk.Align.CENTER) 23 | self._entry_row = None 24 | 25 | self.points = [Point(150, 300), Point(250, 100)] 26 | self.initial_positions = [(50, 350), (350, 50)] 27 | self.dragging = None 28 | 29 | self.controller = Gtk.EventControllerLegacy.new() 30 | self.controller.connect('event', self.on_event) 31 | self.add_controller(self.controller) 32 | self.set_draw_func(self.do_draw) 33 | 34 | def do_draw(self, _, cr, *__): 35 | grid_size = 20 36 | for x in range(50, 350, grid_size): 37 | cr.move_to(x, 50) 38 | cr.line_to(x, 350) 39 | for y in range(50, 350, grid_size): 40 | cr.move_to(50, y) 41 | cr.line_to(350, y) 42 | 43 | cr.set_source_rgba( 44 | fg_color.red, fg_color.green, fg_color.blue, 0.1 # type: ignore 45 | ) 46 | cr.set_line_width(1) 47 | cr.stroke() 48 | 49 | # Diagonal Line 50 | cr.move_to(50, 350) 51 | cr.line_to(350, 50) 52 | cr.set_line_width(2) 53 | cr.stroke() 54 | 55 | # Dots 56 | cr.set_source_rgba( 57 | fg_color.red, fg_color.green, fg_color.blue, 0.8 # type: ignore 58 | ) 59 | cr.set_line_width(3) 60 | for initial, point in zip(self.initial_positions, self.points): 61 | cr.move_to(initial[0], initial[1]) 62 | cr.line_to(point.x, point.y) 63 | cr.stroke() 64 | 65 | # Bezier 66 | cr.set_line_width(4) 67 | cr.move_to(50, 350) 68 | cr.curve_to( 69 | self.points[0].x, 70 | self.points[0].y, 71 | self.points[1].x, 72 | self.points[1].y, 73 | 350, 74 | 50, 75 | ) 76 | cr.set_source_rgba( 77 | accent_color.red, accent_color.green, accent_color.blue, 1 # type: ignore 78 | ) 79 | cr.set_line_width(3) 80 | cr.stroke() 81 | 82 | # Square 83 | cr.rectangle(50, 50, 300, 300) 84 | cr.set_source_rgba( 85 | fg_color.red, fg_color.green, fg_color.blue, 0.1 # type: ignore 86 | ) 87 | cr.set_line_width(3) 88 | cr.stroke() 89 | 90 | # Dots 91 | for point in self.points: 92 | cr.arc(point.x, point.y, 10, 0, 2 * math.pi) 93 | cr.set_source_rgba( 94 | fg_color.red, fg_color.green, fg_color.blue, 1 # type: ignore 95 | ) 96 | cr.fill() 97 | 98 | def on_event(self, newEvent: Gtk.EventControllerLegacy, _) -> None: 99 | Event: Gdk.Event = Gtk.EventController.get_current_event(newEvent) 100 | 101 | match type(Event): 102 | case Gdk.MotionEvent: 103 | self.on_motion_notify(Event) # type: ignore 104 | 105 | case Gdk.ButtonEvent: 106 | 107 | button: int = Event.get_button() # type:ignore 108 | state: Gdk.ModifierType = Event.get_modifier_state() 109 | if button != 1: 110 | return 111 | if state != Gdk.ModifierType.BUTTON1_MASK: 112 | self.on_button_press(Event) 113 | elif state != Gdk.ModifierType.NO_MODIFIER_MASK: 114 | self.on_button_release(Event) 115 | case Gdk.TouchpadEvent: 116 | pass 117 | case Gdk.ScrollEvent: 118 | pass 119 | case _: 120 | print(Event, type(Event)) 121 | pass 122 | 123 | def on_button_release(self, _): 124 | self.dragging = None 125 | 126 | def on_button_press(self, event: Gdk.Event): 127 | x, y = self.get_eventpos(event) 128 | 129 | for i, point in enumerate(self.points): 130 | if (x - point.x) ** 2 + (y - point.y) ** 2 <= 10**2: 131 | self.dragging = i 132 | break 133 | 134 | def on_motion_notify(self, event: Gdk.MotionEvent): 135 | if self.dragging is not None: 136 | x = min(max(self.get_eventpos(event)[0], 50), 350) # type: ignore 137 | y = self.get_eventpos(event)[1] # type: ignore 138 | 139 | self.points[self.dragging].x = x 140 | self.points[self.dragging].y = y 141 | 142 | self.emit('changed') 143 | self.queue_draw() 144 | 145 | def get_bezier(self) -> Tuple[float, float, float, float]: 146 | x0 = (self.points[0].x - 50) / 300 147 | y0 = 1 - (self.points[0].y - 50) / 300 148 | x1 = (self.points[1].x - 50) / 300 149 | y1 = 1 - (self.points[1].y - 50) / 300 150 | return (x0, y0, x1, y1) 151 | 152 | def set_bezier(self, x0: float, y0: float, x1: float, y1: float) -> None: 153 | self.points[0].x = x0 * 300 + 50 154 | self.points[0].y = (1 - y0) * 300 + 50 155 | self.points[1].x = x1 * 300 + 50 156 | self.points[1].y = (1 - y1) * 300 + 50 157 | return self.queue_draw() 158 | 159 | def get_eventpos(self, event) -> Tuple[float, float]: 160 | _, x, y = event.get_position() 161 | return x - 20, y - 45 162 | 163 | 164 | class BezierEditorWindow(Adw.Window): 165 | __gsignals__ = { 166 | 'bezier-updated': (GObject.SignalFlags.RUN_FIRST, None, ()), 167 | } 168 | 169 | def __init__(self) -> None: 170 | super().__init__() 171 | self.editing: Bezier 172 | 173 | self.set_size_request(400, 700) 174 | self.set_modal(True) 175 | self.set_hide_on_close(True) 176 | self.set_resizable(False) 177 | self.set_destroy_with_parent(True) 178 | 179 | self.root = Adw.ToolbarView.new() 180 | self.top_bar = Adw.HeaderBar.new() 181 | self.top_bar.set_title_widget( 182 | Adw.WindowTitle.new('Bezier Settings', '') 183 | ) 184 | 185 | self.bezier_editor = BezierEditor() 186 | self.bezier_editor.connect('changed', self.on_editor_changed) 187 | self.preferences_group = Adw.PreferencesGroup.new() 188 | self.box = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) 189 | self.box.add_css_class('bezier-editor-container') 190 | self.update_graph = True 191 | 192 | self.entry_x0 = Adw.EntryRow.new() 193 | self.entry_y0 = Adw.EntryRow.new() 194 | self.entry_x1 = Adw.EntryRow.new() 195 | self.entry_y1 = Adw.EntryRow.new() 196 | self.entry_x0.set_title('X0') 197 | self.entry_y0.set_title('Y0') 198 | self.entry_x1.set_title('X1') 199 | self.entry_y1.set_title('Y1') 200 | 201 | self.set_content(self.root) 202 | self.root.add_top_bar(self.top_bar) 203 | self.root.set_content(self.box) 204 | self.box.append(self.bezier_editor) 205 | self.box.append(self.preferences_group) 206 | 207 | for i in [self.entry_x0, self.entry_y0, self.entry_x1, self.entry_y1]: 208 | i.connect('changed', self.on_changed) 209 | self.preferences_group.add(i) 210 | 211 | def on_changed(self, _: Gtk.Entry) -> None: 212 | if not self.update_graph: 213 | return 214 | try: 215 | tmp = [] 216 | for i, entry in enumerate( 217 | [ 218 | self.entry_x0, 219 | self.entry_y0, 220 | self.entry_x1, 221 | self.entry_y1, 222 | ] 223 | ): 224 | tmp += [float(entry.get_text())] # type: ignore 225 | if tmp[i] > 2: 226 | tmp[i] = 1 227 | self.bezier_editor.set_bezier(*tmp) 228 | except ValueError: 229 | pass 230 | except Exception as e: 231 | print(e) 232 | 233 | def on_editor_changed(self, _: BezierEditor) -> None: 234 | newValues = self.bezier_editor.get_bezier() 235 | newValues = tuple(round(i, 3) for i in newValues) 236 | 237 | self.update_graph = False 238 | self.entry_x0.set_text(f'{newValues[0]}') 239 | self.entry_y0.set_text(f'{newValues[1]}') 240 | self.entry_x1.set_text(f'{newValues[2]}') 241 | self.entry_y1.set_text(f'{newValues[3]}') 242 | self.update_graph = True 243 | 244 | def on_click(self, _: Gtk.Button) -> None: 245 | pass 246 | 247 | def edit_bezier(self, bezier: Bezier) -> None: 248 | self.editing = bezier 249 | self.bezier_editor.set_bezier(*self.editing.transition) 250 | self.bezier_editor.emit('changed') 251 | 252 | return self.present() 253 | 254 | 255 | MyBezierEditorWindow = BezierEditorWindow() 256 | -------------------------------------------------------------------------------- /app/modules/widgets/BezierEntryRow.py: -------------------------------------------------------------------------------- 1 | from ..imports import Adw, Gtk, Bezier, HyprData, Tuple, string, GObject, Gdk 2 | from .BezierEditor import MyBezierEditorWindow 3 | from .PreferencesGroup import PreferencesGroup 4 | 5 | 6 | class NewBezierDialog(Adw.Dialog): 7 | __gsignals__ = { 8 | 'new-bezier': ( 9 | GObject.SignalFlags.RUN_FIRST, 10 | None, 11 | (str, float, float, float, float), 12 | ), 13 | } 14 | 15 | def __init__(self) -> None: 16 | super().__init__() 17 | 18 | self.set_title('New Curve') 19 | self.set_size_request(400, 200) 20 | 21 | self.root = Adw.ToolbarView.new() 22 | 23 | self.top_bar = Adw.HeaderBar.new() 24 | self.top_bar.set_title_widget(Adw.WindowTitle.new('New Bezier', '')) 25 | 26 | self.body = Adw.PreferencesPage.new() 27 | self.content = PreferencesGroup('', '') 28 | self.entry = Adw.EntryRow.new() 29 | self.entry.set_title('Name') 30 | self.entry.set_text('Epic_Bezier_Name') 31 | self.bezier_entry = Adw.EntryRow.new() 32 | self.bezier_entry.set_title('Bezier') 33 | self.bezier_entry.set_text('cubic-bezier(0.25, 0.75, 0.75, 0.25)') 34 | 35 | self.button = Gtk.Button.new() 36 | self.button.set_label('Add Bezier') 37 | self.button.set_halign(Gtk.Align.CENTER) 38 | self.button.set_hexpand(True) 39 | self.button.add_css_class('suggested-action') 40 | self.button.add_css_class('pill') 41 | self.button.set_margin_top(20) 42 | self.button.connect('clicked', self.on_activate) 43 | 44 | self.root.add_top_bar(self.top_bar) 45 | self.root.set_content(self.body) 46 | self.body.add(self.content) 47 | self.content.add(self.entry) 48 | self.content.add(self.bezier_entry) 49 | self.content.add(self.button) 50 | self.set_child(self.root) 51 | 52 | def on_activate(self, _) -> None: 53 | state, bezier = self.parse_bezier( 54 | self.bezier_entry.get_text() # type: ignore 55 | ) 56 | name = self.parse_name(self.entry.get_text()) 57 | 58 | if state and name and name not in HyprData.beziers.keys(): 59 | self.emit('new-bezier', name, *bezier) 60 | self.close() 61 | 62 | def parse_bezier( 63 | self, bezier: str 64 | ) -> Tuple[bool, Tuple[float, float, float, float]]: 65 | bezier = bezier.replace(' ', '').lower() 66 | if bezier.startswith('cubic-bezier'): 67 | bezier = bezier[12:] 68 | bezier = ''.join( 69 | i for i in bezier if i not in string.ascii_lowercase + '()' 70 | ) 71 | try: 72 | x0, y0, x1, y1 = bezier.split(',') 73 | return (True, tuple(map(float, (x0, y0, x1, y1)))) # type: ignore 74 | except ValueError: 75 | pass 76 | except Exception as e: 77 | print(e) 78 | return (False, (0, 0, 0, 0)) 79 | 80 | def parse_name(self, text: str) -> str: 81 | text = text.replace(' ', '') 82 | text = ''.join(i for i in text if i in string.ascii_letters + '_') 83 | return text 84 | 85 | 86 | BezierAddDialog = NewBezierDialog() 87 | 88 | 89 | class BezierPreviewRow(Adw.ActionRow): 90 | def __init__( 91 | self, 92 | new_bezier: Bezier, 93 | ) -> None: 94 | super().__init__() 95 | self.bezier = new_bezier 96 | self.edit_button = Gtk.Button.new_from_icon_name( 97 | 'document-edit-symbolic' 98 | ) 99 | self.del_button = Gtk.Button.new_from_icon_name('user-trash-symbolic') 100 | self.copy_button = Gtk.Button.new_from_icon_name('edit-copy-symbolic') 101 | 102 | self.set_title(self.bezier.name) 103 | self.set_subtitle( 104 | 'cubic-bezier({})'.format( 105 | ', '.join(map(str, self.bezier.transition)) 106 | ) 107 | ) 108 | 109 | for i in [ 110 | self.copy_button, 111 | self.edit_button, 112 | self.del_button, 113 | ]: 114 | i.add_css_class('flat') 115 | i.set_valign(Gtk.Align.CENTER) 116 | i.set_focusable(True) 117 | self.add_suffix(i) 118 | 119 | 120 | class BezierGroup(PreferencesGroup): 121 | def __init__(self): 122 | super().__init__( 123 | 'Curves', 124 | 'Define your own bezier curves.', 125 | ) 126 | self.children_count = 0 127 | 128 | self.beziers = HyprData.beziers 129 | 130 | self.button = Gtk.Button.new_from_icon_name('list-add-symbolic') 131 | self.button.add_css_class('flat') 132 | self.button.set_valign(Gtk.Align.CENTER) 133 | self.set_header_suffix(self.button) 134 | 135 | for i in self.beziers.values(): 136 | tmp = BezierPreviewRow(i) 137 | if i.name.lower() == 'linear': 138 | tmp.del_button.set_sensitive(False) 139 | tmp.edit_button.set_sensitive(False) 140 | 141 | tmp.del_button.connect('clicked', self.on_clicked_child, tmp) 142 | tmp.edit_button.connect('clicked', self.on_clicked_child, tmp) 143 | tmp.copy_button.connect('clicked', self.on_clicked_child, tmp) 144 | self.add(tmp) 145 | 146 | if 'linear' not in list(map(str.lower, self.beziers.keys())): 147 | tmp = BezierPreviewRow(Bezier('linear', (0, 0, 1, 1))) 148 | tmp.del_button.set_sensitive(False) 149 | tmp.edit_button.set_sensitive(False) 150 | tmp.del_button.connect('clicked', self.on_clicked_child, tmp) 151 | tmp.edit_button.connect('clicked', self.on_clicked_child, tmp) 152 | tmp.copy_button.connect('clicked', self.on_clicked_child, tmp) 153 | 154 | self.button.connect('clicked', self.on_clicked) 155 | BezierAddDialog.connect('new-bezier', self.on_new_bezier) 156 | MyBezierEditorWindow.connect('bezier-updated', self.on_updated_bezier) 157 | 158 | def on_clicked(self, _: Gtk.Button) -> None: 159 | return BezierAddDialog.present(self.get_root()) # type: ignore 160 | 161 | def on_new_bezier( 162 | self, _, name: str, x0: float, y0: float, x1: float, y1: float 163 | ) -> None: 164 | return self.add(BezierPreviewRow(Bezier(name, (x0, y0, x1, y1)))) 165 | 166 | def on_updated_bezier( 167 | self, _, name: str, x0: float, y0: float, x1: float, y1: float 168 | ): 169 | pass 170 | 171 | def on_clicked_child( 172 | self, 173 | button: Gtk.Button, 174 | child: BezierPreviewRow, 175 | ): 176 | 177 | if button is child.del_button: 178 | return self.remove(child) 179 | elif button is child.copy_button: 180 | Gdk.Clipboard.new().set_text('epic') 181 | 182 | elif button is child.edit_button: 183 | return MyBezierEditorWindow.edit_bezier(child.bezier) 184 | 185 | def add_bezier(self, new_bezier: Bezier) -> None: # type: ignore 186 | self.children_count += 1 187 | return self.add(BezierPreviewRow(new_bezier)) 188 | 189 | def update_default(self) -> None: 190 | pass 191 | -------------------------------------------------------------------------------- /app/modules/widgets/ButtonRow.py: -------------------------------------------------------------------------------- 1 | from ..imports import Adw 2 | from .Icon import Icon 3 | 4 | 5 | class ButtonRow(Adw.ActionRow): 6 | def __init__( 7 | self, 8 | prefix: str, 9 | title: str, 10 | subtitle: str, 11 | on_activated=lambda self: self, 12 | ) -> None: 13 | super().__init__() 14 | self.set_title(title) 15 | self.set_subtitle(subtitle) 16 | self.set_activatable(True) 17 | self.add_prefix(Icon(prefix)) 18 | self.add_suffix(Icon('go-next-symbolic')) 19 | self.connect('activated', on_activated) 20 | -------------------------------------------------------------------------------- /app/modules/widgets/CheckButtonImage.py: -------------------------------------------------------------------------------- 1 | from ..imports import Gtk 2 | from .Icon import Icon 3 | 4 | 5 | class CheckButtonImage(Gtk.Box): 6 | def __init__(self, title: str, image: str) -> None: 7 | super().__init__() 8 | self.set_spacing(12) 9 | self.set_margin_top(6) 10 | self.set_orientation(Gtk.Orientation.VERTICAL) 11 | self.checkbutton = Gtk.CheckButton.new_with_label(title) 12 | self.checkbutton.get_first_child().set_margin_start(12) 13 | self.checkbutton.get_last_child().set_margin_start(6) 14 | 15 | self.img = Icon(image) 16 | self.img.add_css_class('icon') 17 | self.img.set_pixel_size(200) 18 | 19 | self.img.set_hexpand(True) 20 | self.img.set_vexpand(True) 21 | 22 | self.img_container = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 0) 23 | self.img_container.add_css_class('background') 24 | self.img_container.add_css_class('frame') 25 | self.img_container.append(self.img) 26 | 27 | self.append(self.img_container) 28 | self.append(self.checkbutton) 29 | -------------------------------------------------------------------------------- /app/modules/widgets/ColorEntryRow.py: -------------------------------------------------------------------------------- 1 | from .CustomToastOverlay import ToastOverlay 2 | from ..imports import Adw, Gtk, HyprData, Setting, Color 3 | from ..utils import ParseColor 4 | 5 | 6 | class ColorEntryRow(Adw.ActionRow): 7 | def __init__(self, title: str, description: str, section: str) -> None: 8 | super().__init__() 9 | 10 | ToastOverlay.instances.append(self) 11 | 12 | self.set_title(title) 13 | self.set_subtitle(description) 14 | 15 | self.entry = Gtk.Entry.new() 16 | self.stack = Gtk.Stack.new() 17 | self.button_showcolor = Gtk.ToggleButton.new() 18 | self.colorbutton = Gtk.ColorButton.new() 19 | self.colorbutton.set_use_alpha(True) 20 | self.gdkcolor = self.colorbutton.get_rgba() # type:ignore 21 | 22 | self.add_suffix(self.stack) 23 | self.add_suffix(self.button_showcolor) 24 | 25 | self.button_showcolor.set_icon_name('document-edit-symbolic') 26 | self.button_showcolor.add_css_class('flat') 27 | self.button_showcolor.set_valign(Gtk.Align.CENTER) 28 | 29 | self.entry.set_valign(Gtk.Align.CENTER) 30 | self.colorbutton.set_valign(Gtk.Align.CENTER) 31 | 32 | self.stack.set_hhomogeneous(False) 33 | self.stack.set_interpolate_size(True) 34 | self.stack.add_named(self.colorbutton, 'color-button') 35 | self.stack.add_named(self.entry, 'entry') 36 | self.stack.set_transition_type(Gtk.StackTransitionType.CROSSFADE) 37 | 38 | self.section = section 39 | opt = HyprData.get_option(self.section) 40 | 41 | if not opt: 42 | opt = Setting(self.section, Color('00', '00', '00', '00')) 43 | HyprData.new_option(opt) 44 | 45 | if isinstance(opt.value, (Color)): 46 | self.color: Color = opt.value 47 | 48 | self.entry.set_text('#' + self.color.hex) 49 | self.gdkcolor = ParseColor.hex_to_gdk_rgba(self.color.hex) 50 | self.colorbutton.set_rgba(self.gdkcolor) # type: ignore 51 | 52 | self._default = (self.entry.get_text(), False) # type: ignore 53 | self.entry.connect('changed', self.on_changed) 54 | self.colorbutton.connect('color-set', self.on_color_set) 55 | self.button_showcolor.connect('toggled', self.on_toggled) 56 | 57 | def on_toggled(self, _: Gtk.ToggleButton) -> None: 58 | if self.button_showcolor.get_active(): 59 | return self.stack.set_visible_child_name('entry') 60 | return self.stack.set_visible_child_name('color-button') 61 | 62 | def on_changed(self, _: Gtk.Entry) -> None: 63 | 64 | if not self.gdkcolor.parse(self.entry.get_text()): # type: ignore 65 | return 66 | 67 | self.colorbutton.set_rgba(self.gdkcolor) # type: ignore 68 | 69 | color = ParseColor.gdk_rgba_to_hex(self.gdkcolor).removeprefix('#') 70 | self.color.r = color[0:2] 71 | self.color.g = color[2:4] 72 | self.color.b = color[4:6] 73 | self.color.a = color[6:8] 74 | 75 | HyprData.set_option(self.section, self.color) 76 | 77 | return self.add_change() 78 | 79 | def on_color_set(self, _: Gtk.ColorButton) -> None: 80 | 81 | color = ParseColor.gdk_rgba_to_hex(self.gdkcolor).removeprefix('#') 82 | 83 | self.color.r = color[0:2] 84 | self.color.g = color[2:4] 85 | self.color.b = color[4:6] 86 | self.color.a = color[6:8] 87 | 88 | self.entry.set_text(ParseColor.gdk_rgba_to_hex(self.gdkcolor)) 89 | 90 | HyprData.set_option(self.section, self.color) 91 | 92 | return self.add_change() 93 | 94 | def add_change(self) -> None: 95 | if self._default[0] != self.entry.get_text(): # type: ignore 96 | if not self._default[1]: 97 | ToastOverlay.add_change() 98 | self._default = (self._default[0], True) 99 | else: 100 | ToastOverlay.del_change() 101 | self._default = (self._default[0], False) 102 | return 103 | 104 | def update_default(self) -> None: 105 | self._default = (self.entry.get_text(), False) # type: ignore 106 | -------------------------------------------------------------------------------- /app/modules/widgets/ColorExpanderRow.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gdk 2 | from ..imports import Adw, Gtk, HyprData, Gradient, Color, Setting 3 | from ..utils import ParseColor 4 | from .CustomToastOverlay import ToastOverlay 5 | 6 | 7 | class ColorExpanderRow(Adw.ExpanderRow): 8 | class ColorEntryRow(Adw.EntryRow): 9 | def __init__(self, parent: 'ColorExpanderRow', new_color: str = ''): 10 | super().__init__() 11 | self.parent = parent 12 | self.set_title('Color') 13 | self.gdkcolor = Gdk.RGBA(0, 0, 0, 1) # type:ignore 14 | ToastOverlay.instances.append(self) 15 | 16 | if new_color: 17 | self._default = new_color 18 | self.set_text(self._default) 19 | self.set_title('Color') 20 | self.on_changed(self) 21 | 22 | self.button = Gtk.Button.new() 23 | 24 | self.button.set_icon_name('user-trash-symbolic') 25 | self.button.set_can_focus(False) 26 | self.button.add_css_class('flat') 27 | self.button.set_valign(Gtk.Align.CENTER) 28 | 29 | self.add_suffix(self.button) 30 | 31 | self.button.connect('clicked', self.on_clicked) 32 | self.connect('changed', self.on_changed) 33 | 34 | def on_clicked(self, *_: Gtk.Button): 35 | return self.parent.remove(self) 36 | 37 | def on_changed(self, *_: 'ColorExpanderRow.ColorEntryRow'): 38 | if self.gdkcolor.parse(self.get_text()): 39 | self._default = ParseColor.gdk_rgba_to_hex(self.gdkcolor) 40 | return self.set_title( 41 | f' Color ' 42 | ) 43 | return self.set_title( 44 | f' Color ' 45 | ) 46 | 47 | def get_text(self) -> str: 48 | return getattr(super(), 'get_text', lambda: '')() 49 | 50 | def update_default(self): 51 | self._default = ParseColor.gdk_rgba_to_hex(self.gdkcolor) 52 | 53 | def __init__(self, title: str, subtitle: str, section: str): 54 | super().__init__() 55 | ToastOverlay.instances.append(self) 56 | self.section = section 57 | 58 | self.button = Adw.ActionRow.new() 59 | self.button.set_activatable(True) 60 | self.button.set_icon_name('list-add-symbolic') 61 | self.button.set_title('Add Color') 62 | self.button.set_hexpand(True) 63 | self.button.get_child().set_halign(Gtk.Align.CENTER) 64 | 65 | self.set_title(title) 66 | self.set_subtitle(subtitle) 67 | self.add_row(self.button) 68 | self.button.connect( 69 | 'activated', 70 | lambda *_: self.add_row( 71 | ColorExpanderRow.ColorEntryRow(self, '#777777FF') 72 | ), 73 | ) 74 | 75 | opt = HyprData.get_option(self.section) 76 | 77 | if not opt: 78 | opt = Setting(self.section, 0) 79 | HyprData.new_option(opt) 80 | 81 | if isinstance(opt.value, (Gradient)): 82 | color: Color 83 | for color in opt.value.colors: 84 | self.add_row( 85 | ColorExpanderRow.ColorEntryRow(self, '#' + color.hex) 86 | ) 87 | 88 | def update_default(self) -> None: 89 | pass 90 | -------------------------------------------------------------------------------- /app/modules/widgets/CustomToastOverlay.py: -------------------------------------------------------------------------------- 1 | from ..imports import Adw, HyprData 2 | 3 | 4 | class CustomToastOverlay: 5 | instances = [] 6 | 7 | def __init__(self) -> None: 8 | self.changes = 0 9 | self._instance = Adw.ToastOverlay.new() 10 | self.toast = Adw.Toast.new('You have 0 unsaved changes!') 11 | self.toast.connect('button-clicked', self.save_changes) 12 | self.toast.set_button_label('Save now') 13 | self.toast.set_timeout(0) 14 | 15 | @property 16 | def instance(self) -> Adw.ToastOverlay: 17 | return self._instance 18 | 19 | def show_toast(self) -> None: 20 | self.instance.add_toast(self.toast) 21 | 22 | def hide_toast(self) -> None: 23 | self.toast.dismiss() 24 | 25 | def add_change(self) -> None: 26 | self.changes += 1 27 | self.toast.set_title(f'You have {self.changes} unsaved changes!') 28 | return self.show_toast() 29 | 30 | def del_change(self) -> None: 31 | self.changes -= 1 32 | self.toast.set_title(f'You have {self.changes} unsaved changes!') 33 | if self.changes == 0: 34 | return self.hide_toast() 35 | 36 | # After calling this function, each widget updates its new default value. 37 | def save_changes(self, *_) -> None: 38 | self.changes = 0 39 | self.hide_toast() 40 | 41 | for i in CustomToastOverlay.instances: 42 | i.update_default() 43 | return HyprData.save_all() 44 | 45 | 46 | ToastOverlay = CustomToastOverlay() 47 | -------------------------------------------------------------------------------- /app/modules/widgets/ExpanderRow.py: -------------------------------------------------------------------------------- 1 | from ..imports import Adw 2 | 3 | 4 | class ExpanderRow(Adw.ExpanderRow): 5 | def __init__(self, title: str, subtitle: str) -> None: 6 | super().__init__() 7 | self.set_title(title) 8 | self.set_subtitle(subtitle) 9 | -------------------------------------------------------------------------------- /app/modules/widgets/Icon.py: -------------------------------------------------------------------------------- 1 | from ..imports import Literal, Gio, GLib, Gtk 2 | 3 | 4 | def Icon( 5 | name: str, size: Literal["large", "normal", "inherit"] = "normal" 6 | ) -> Gtk.Image: 7 | new_icon = Gtk.Image.new() 8 | new_icon.filepath = "{}/icons/{}.svg".format(__file__[:-24], name) 9 | 10 | if GLib.file_test(new_icon.filepath, GLib.FileTest.EXISTS): 11 | new_icon.set_from_gicon( 12 | Gio.FileIcon.new(Gio.File.new_for_path(new_icon.filepath)) 13 | ) 14 | else: 15 | new_icon.set_from_icon_name(name) 16 | match size: 17 | case "large": 18 | new_icon.set_icon_size(Gtk.IconSize.LARGE) 19 | case "normal": 20 | new_icon.set_icon_size(Gtk.IconSize.NORMAL) 21 | case "inherit": 22 | new_icon.set_icon_size(Gtk.IconSize.INHERIT) 23 | 24 | return new_icon 25 | -------------------------------------------------------------------------------- /app/modules/widgets/InfoButton.py: -------------------------------------------------------------------------------- 1 | from ..imports import Gtk 2 | 3 | 4 | class InfoButton(Gtk.MenuButton): 5 | def __init__(self, text: str) -> None: 6 | super().__init__( 7 | icon_name='help-info-symbolic', 8 | ) 9 | self.set_icon_name('help-info-symbolic') 10 | self.set_sensitive(True) 11 | self.set_valign(Gtk.Align.CENTER) 12 | self.set_halign(Gtk.Align.CENTER) 13 | self.add_css_class('flat') 14 | self.set_popover() 15 | self.popover = Gtk.Popover.new() 16 | self.label = Gtk.Label.new(text) 17 | self.label.set_markup(text) 18 | self.label.set_wrap(True) 19 | self.label.set_max_width_chars(40) 20 | 21 | self.popover.set_child(self.label) 22 | self.set_popover(self.popover) 23 | -------------------------------------------------------------------------------- /app/modules/widgets/PreferencesGroup.py: -------------------------------------------------------------------------------- 1 | from ..imports import Adw 2 | 3 | 4 | class PreferencesGroup(Adw.PreferencesGroup): 5 | def __init__(self, title: str, description: str): 6 | super().__init__() 7 | self.set_title(title) 8 | self.set_description(description) 9 | -------------------------------------------------------------------------------- /app/modules/widgets/SpinRow.py: -------------------------------------------------------------------------------- 1 | from types import new_class 2 | from ..imports import Gtk, Union, Type, Adw, Setting, HyprData 3 | from .CustomToastOverlay import ToastOverlay 4 | 5 | 6 | def Adjustment( 7 | section: Union[str, None], 8 | data_type: Type[Union[int, float]] = int, 9 | min: Union[int, float] = 0, 10 | max: Union[int, float] = 255, 11 | ): 12 | new_adjustment = Gtk.Adjustment(lower=min, upper=max, page_size=0) 13 | 14 | new_adjustment.data_type = data_type 15 | new_adjustment.section = section 16 | 17 | if data_type.__name__ == "int": 18 | new_adjustment.set_step_increment(1) 19 | new_adjustment.set_page_increment(10) 20 | else: 21 | new_adjustment.set_step_increment(0.1) 22 | new_adjustment.set_page_increment(1.0) 23 | 24 | ToastOverlay.instances.append(new_adjustment) 25 | 26 | if new_adjustment.section is not None: 27 | opt = HyprData.get_option(new_adjustment.section) 28 | 29 | if not opt: 30 | opt = Setting(new_adjustment.section, 1) 31 | HyprData.new_option(opt) 32 | 33 | if isinstance(opt.value, (int, float)): 34 | new_adjustment.set_value(opt.value) 35 | 36 | new_adjustment._default = (opt.value, False) 37 | else: 38 | new_adjustment._default = (0, False) 39 | 40 | def update_default(*args, **kwargs) -> None: 41 | new_adjustment._default = (new_adjustment.get_value(), False) 42 | 43 | def on_value_changed(self): 44 | if new_adjustment._default[0] != new_adjustment.get_value(): 45 | if not new_adjustment._default[1]: 46 | ToastOverlay.add_change() 47 | new_adjustment._default = (new_adjustment._default[0], True) 48 | else: 49 | ToastOverlay.del_change() 50 | new_adjustment._default = (self._default[0], False) 51 | 52 | if new_adjustment.section is None: 53 | return 54 | 55 | if self.data_type.__name__ == "int": 56 | return HyprData.set_option( 57 | new_adjustment.section, round(new_adjustment.get_value()) 58 | ) 59 | return HyprData.set_option(new_adjustment.section, new_adjustment.get_value()) 60 | 61 | new_adjustment.update_default = update_default 62 | new_adjustment.connect("value-changed", on_value_changed) 63 | 64 | return new_adjustment 65 | 66 | 67 | def SpinRow( 68 | title: str, 69 | subtitle: str, 70 | section: str, 71 | data_type: Type[Union[int, float]] = int, 72 | min: Union[int, float] = 0, 73 | max: Union[int, float] = 255, 74 | decimal_digits: int = 2, 75 | ): 76 | new_spinrow = Adw.SpinRow(adjustment=Adjustment(section, data_type, min, max),title=title, 77 | subtitle=subtitle 78 | ) 79 | 80 | 81 | if data_type.__name__ == "float": 82 | new_spinrow.set_digits(decimal_digits) 83 | 84 | return new_spinrow 85 | 86 | 87 | class _Adjustment(Gtk.Adjustment): 88 | def __init__( 89 | self, 90 | section: Union[str, None], 91 | data_type: Type[Union[int, float]] = int, 92 | min: Union[int, float] = 0, 93 | max: Union[int, float] = 255, 94 | ): 95 | super().__init__() 96 | self.data_type = data_type 97 | self.set_lower(min) 98 | self.set_upper(max) 99 | self.set_page_size(0) 100 | 101 | if data_type.__name__ == "int": 102 | self.set_step_increment(1) 103 | self.set_page_increment(10) 104 | else: 105 | self.set_step_increment(0.1) 106 | self.set_page_increment(1.0) 107 | 108 | ToastOverlay.instances.append(self) 109 | 110 | self.section = section 111 | 112 | if self.section is not None: 113 | opt = HyprData.get_option(self.section) 114 | 115 | if not opt: 116 | opt = Setting(self.section, 1) 117 | HyprData.new_option(opt) 118 | 119 | if isinstance(opt.value, (int, float)): 120 | self.set_value(opt.value) 121 | 122 | self._default = (opt.value, False) 123 | else: 124 | self._default = (0, False) 125 | 126 | self.connect("value-changed", self.on_value_changed) 127 | 128 | def on_value_changed(self, _): 129 | if self._default[0] != self.get_value(): 130 | if not self._default[1]: 131 | ToastOverlay.add_change() 132 | self._default = (self._default[0], True) 133 | else: 134 | ToastOverlay.del_change() 135 | self._default = (self._default[0], False) 136 | 137 | if self.section is None: 138 | return 139 | 140 | if self.data_type.__name__ == "int": 141 | return HyprData.set_option(self.section, round(self.get_value())) 142 | return HyprData.set_option(self.section, self.get_value()) 143 | 144 | def update_default(self) -> None: 145 | self._default = (self.get_value(), False) 146 | 147 | 148 | class _SpinRow: 149 | def __init__( 150 | self, 151 | title: str, 152 | subtitle: str, 153 | section: str, 154 | data_type: Type[Union[int, float]] = int, 155 | min: Union[int, float] = 0, 156 | max: Union[int, float] = 255, 157 | decimal_digits: int = 2, 158 | ): 159 | self._instance = Adw.SpinRow() 160 | self.instance.set_adjustment(Adjustment(section, data_type, min, max)) 161 | self.instance.set_title(title) 162 | self.instance.set_subtitle(subtitle) 163 | 164 | if data_type.__name__ == "float": 165 | self.instance.set_digits(decimal_digits) 166 | 167 | @property 168 | def instance(self) -> Adw.SpinRow: 169 | return self._instance 170 | -------------------------------------------------------------------------------- /app/modules/widgets/SwitchRow.py: -------------------------------------------------------------------------------- 1 | from .CustomToastOverlay import ToastOverlay 2 | from ..imports import Adw, HyprData, Setting 3 | 4 | 5 | def SwitchRow(title: str, subtitle: str, section: str, *, invert: bool = False): 6 | new_switchrow = Adw.SwitchRow(title = title, subtitle = subtitle) 7 | 8 | ToastOverlay.instances.append(new_switchrow) 9 | new_switchrow._invert = invert 10 | new_switchrow.section = section 11 | 12 | 13 | opt = HyprData.get_option(new_switchrow.section) 14 | 15 | if not opt: 16 | opt = Setting(new_switchrow.section, False) 17 | HyprData.new_option(opt) 18 | 19 | if new_switchrow._invert: 20 | new_switchrow.set_active(not opt.value) 21 | else: 22 | new_switchrow.set_active(bool(opt.value)) 23 | 24 | new_switchrow._default = new_switchrow.get_active() 25 | 26 | def on_active(*args, **kwargs): 27 | if new_switchrow.get_active() != new_switchrow._default: 28 | ToastOverlay.add_change() 29 | else: 30 | ToastOverlay.del_change() 31 | 32 | if new_switchrow._invert: 33 | return HyprData.set_option( 34 | new_switchrow.section, not new_switchrow.get_active() 35 | ) 36 | 37 | return HyprData.set_option(new_switchrow.section, new_switchrow.get_active()) 38 | 39 | def update_default(*args, **kwargs): 40 | new_switchrow._default = new_switchrow.get_active() 41 | 42 | new_switchrow.connect("notify::active", on_active) 43 | new_switchrow.update_default = update_default 44 | return new_switchrow 45 | 46 | 47 | class _SwitchRow: 48 | def __init__( 49 | self, title: str, subtitle: str, section: str, *, invert: bool = False 50 | ) -> None: 51 | super().__init__() 52 | ToastOverlay.instances.append(self) 53 | self.__invert = invert 54 | self._instance = Adw.SwitchRow() 55 | self.instance.set_title(title) 56 | self.instance.set_subtitle(subtitle) 57 | self.section = section 58 | 59 | opt = HyprData.get_option(self.section) 60 | 61 | if not opt: 62 | opt = Setting(self.section, False) 63 | HyprData.new_option(opt) 64 | 65 | if self.__invert: 66 | self.instance.set_active(not opt.value) 67 | else: 68 | self.instance.set_active(bool(opt.value)) 69 | 70 | self._default = self.instance.get_active() 71 | self.instance.connect("notify::active", self.on_activate) 72 | 73 | def on_activate(self, *_): 74 | if self.instance.get_active() != self._default: 75 | ToastOverlay.add_change() 76 | else: 77 | ToastOverlay.del_change() 78 | if self.__invert: 79 | return HyprData.set_option(self.section, not self.instance.get_active()) 80 | return HyprData.set_option(self.section, self.instance.get_active()) 81 | -------------------------------------------------------------------------------- /app/modules/widgets/__init__.py: -------------------------------------------------------------------------------- 1 | # ruff: noqa 2 | # Some widgets have a default value, to check 3 | # if their new value is different from the initial one. 4 | # If it is, then the changes count of the toast is increased; 5 | # if not, then the changes count of the toast is decreased. 6 | # 7 | # https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/style-classes.html 8 | 9 | from .CustomToastOverlay import ToastOverlay 10 | from .BezierEntryRow import BezierAddDialog, BezierGroup 11 | from .BezierEditor import MyBezierEditorWindow 12 | from .Icon import Icon 13 | from .ButtonRow import ButtonRow 14 | from .SwitchRow import SwitchRow 15 | from .ColorEntryRow import ColorEntryRow 16 | from .ColorExpanderRow import ColorExpanderRow 17 | from .CheckButtonImage import CheckButtonImage 18 | from .PreferencesGroup import PreferencesGroup 19 | from .SpinRow import SpinRow 20 | from .InfoButton import InfoButton 21 | from .ExpanderRow import ExpanderRow 22 | -------------------------------------------------------------------------------- /app/style.css: -------------------------------------------------------------------------------- 1 | .hyprland-settings { 2 | font-weight: 700; 3 | } 4 | 5 | .bezier-editor-container { 6 | margin: 0 20px; 7 | } 8 | 9 | .bezier-preferences-group { 10 | padding: 0 20px; 11 | } 12 | 13 | .transparent-button { 14 | background: unset; 15 | background-color: unset; 16 | font-weight: 500; 17 | } 18 | 19 | .checkbutton-container { 20 | margin: 0 6px; 21 | } 22 | 23 | .list-box-scroll { 24 | background: transparent; 25 | min-width: 220px; 26 | } 27 | 28 | .list-box-scroll .list-box-row { 29 | border-radius: 6px; 30 | padding: 0 12px; 31 | margin: 0 6px 2px 6px; 32 | min-height: 40px; 33 | } 34 | 35 | .list-box-scroll .list-box-row label { 36 | font-size: 14.6px; 37 | font-weight: 400; 38 | } 39 | 40 | .list-box-scroll separator { 41 | margin: 6px; 42 | } 43 | 44 | .unsetted-rowbox { 45 | all: unset; 46 | } 47 | 48 | /*# sourceMappingURL=style.css.map */ 49 | -------------------------------------------------------------------------------- /app/style.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sourceRoot":"","sources":["style.scss"],"names":[],"mappings":"AAAA;EACE;;;AAIF;EACE;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;AAEA;EACE;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAIJ;EACE","file":"style.css"} -------------------------------------------------------------------------------- /app/style.scss: -------------------------------------------------------------------------------- 1 | .bezier-editor-container { 2 | margin: 0 20px; 3 | } 4 | 5 | .bezier-preferences-group { 6 | padding: 0 20px; 7 | } 8 | 9 | 10 | .hyprland-settings { 11 | font-weight: 700; 12 | } 13 | 14 | 15 | .transparent-button { 16 | background: unset; 17 | background-color: unset; 18 | font-weight: 500; 19 | } 20 | 21 | .checkbutton-container { 22 | margin: 0 6px; 23 | } 24 | 25 | .list-box-scroll { 26 | background: transparent; 27 | min-width: 220px; 28 | 29 | .list-box-row { 30 | border-radius: 6px; 31 | padding: 0 12px; 32 | margin: 0 6px 2px 6px; 33 | min-height: 40px; 34 | 35 | label { 36 | font-size: 14.6px; 37 | font-weight: 400; 38 | } 39 | } 40 | 41 | separator { 42 | margin: 6px; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /img/app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyprland-community/hyprset/3b5a875058e753193deb806c9739dbea18e2f897/img/app.png --------------------------------------------------------------------------------