├── .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 | 
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 |
8 |
--------------------------------------------------------------------------------
/app/icons/_master.svg:
--------------------------------------------------------------------------------
1 |
2 |
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 |
15 |
--------------------------------------------------------------------------------
/app/icons/dwindle.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
--------------------------------------------------------------------------------
/app/icons/graph-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/app/icons/key4-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/app/icons/master.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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
--------------------------------------------------------------------------------