Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1edfb6fe4 | ||
|
|
ccf46c40ef |
+36
-11
@@ -36,7 +36,9 @@ hl.config({
|
||||
and "rgba(a8aecb99)" or "rgba(3b426199)",
|
||||
},
|
||||
|
||||
resize_on_border = true,
|
||||
resize_on_border = prefs.get("resizeOnBorder", true),
|
||||
extend_border_grab_area = prefs.getInt("borderGrabArea", 15),
|
||||
hover_icon_on_border = prefs.get("hoverIconOnBorder", true),
|
||||
|
||||
-- Enables the per-window "immediate" rule used for games in rules.lua.
|
||||
-- Harmless on its own; tearing only happens where a rule opts in.
|
||||
@@ -44,16 +46,22 @@ hl.config({
|
||||
|
||||
layout = "dwindle",
|
||||
|
||||
snap = { enabled = true },
|
||||
snap = {
|
||||
enabled = true,
|
||||
window_gap = prefs.getInt("snapWindowGap", 10),
|
||||
monitor_gap = prefs.getInt("snapMonitorGap", 10),
|
||||
respect_gaps = prefs.get("snapRespectGaps", false),
|
||||
},
|
||||
},
|
||||
|
||||
decoration = {
|
||||
-- 18 to match the shell's popover radius, so a window and a panel sitting
|
||||
-- next to each other read as the same object family.
|
||||
rounding = prefs.get("windowRounding", 18),
|
||||
rounding_power = 2,
|
||||
rounding_power = prefs.get("roundingPower", 2),
|
||||
|
||||
active_opacity = 1.0,
|
||||
active_opacity = prefs.get("activeOpacity", 1.0),
|
||||
fullscreen_opacity = prefs.get("fullscreenOpacity", 1.0),
|
||||
inactive_opacity = prefs.get("inactiveOpacity", 1.0),
|
||||
|
||||
blur = {
|
||||
@@ -83,9 +91,13 @@ hl.config({
|
||||
shadow = {
|
||||
enabled = prefs.get("shadowEnabled", true),
|
||||
range = prefs.get("shadowRange", 20),
|
||||
render_power = 3,
|
||||
sharp = false,
|
||||
render_power = prefs.getInt("shadowRenderPower", 3),
|
||||
sharp = prefs.get("shadowSharp", false),
|
||||
color = "rgba(15161eee)",
|
||||
-- Deliberately not a setting: a two-axis offset needs a control we
|
||||
-- do not have, and a slider bound to half a value is worse than
|
||||
-- leaving it alone. SystemSettings understands the vec2 shape
|
||||
-- already, so adding it later is only a matter of the widget.
|
||||
offset = { 0, 4 },
|
||||
scale = 1.0,
|
||||
},
|
||||
@@ -110,14 +122,27 @@ hl.config({
|
||||
dwindle = {
|
||||
-- Keep the split orientation a window was created with. Closest match
|
||||
-- to how the Forge extension behaved on GNOME.
|
||||
preserve_split = true,
|
||||
preserve_split = prefs.get("preserveSplit", true),
|
||||
smart_resizing = true,
|
||||
},
|
||||
|
||||
-- Only in effect when the tiling layout is "master". Panama ships dwindle,
|
||||
-- but Settings offers master as a choice, and a layout you can select and
|
||||
-- cannot configure is barely a choice at all.
|
||||
master = {
|
||||
mfact = prefs.get("masterFactor", 0.55),
|
||||
orientation = prefs.get("masterOrientation", "left"),
|
||||
new_status = prefs.get("masterNewStatus", "slave"),
|
||||
new_on_top = prefs.get("masterNewOnTop", false),
|
||||
},
|
||||
|
||||
misc = {
|
||||
force_default_wallpaper = 0,
|
||||
disable_hyprland_logo = true,
|
||||
disable_splash_rendering = true,
|
||||
-- Stored as "show the logo / show the splash" and written as Hyprland's
|
||||
-- `disable_*`, matching the `invert` flag on these entries in
|
||||
-- PreferenceSchema so both sides agree about which way round they are.
|
||||
disable_hyprland_logo = not prefs.get("hyprlandLogo", false),
|
||||
disable_splash_rendering = not prefs.get("hyprlandSplash", false),
|
||||
|
||||
-- Same setting as Theme.fontFamily in the shell. If only the QML side
|
||||
-- followed the preference, the compositor and the shell would disagree
|
||||
@@ -172,8 +197,8 @@ hl.config({
|
||||
},
|
||||
|
||||
ecosystem = {
|
||||
no_update_news = true,
|
||||
no_donation_nag = true,
|
||||
no_update_news = not prefs.get("hyprlandUpdateNews", false),
|
||||
no_donation_nag = not prefs.get("hyprlandDonationNag", false),
|
||||
},
|
||||
|
||||
xwayland = {
|
||||
|
||||
@@ -206,6 +206,147 @@ Singleton {
|
||||
hypr: { path: ["decoration", "inactive_opacity"], option: "decoration:inactive_opacity", readAs: "float" }
|
||||
},
|
||||
|
||||
{
|
||||
key: "activeOpacity", type: "real", def: 1.0, min: 0.5, max: 1.0, step: 0.05,
|
||||
group: "windows",
|
||||
label: "Focused window opacity",
|
||||
detail: "Fade even the focused window; 1.0 is fully opaque",
|
||||
hypr: { path: ["decoration", "active_opacity"], option: "decoration:active_opacity", readAs: "float" }
|
||||
},
|
||||
{
|
||||
key: "fullscreenOpacity", type: "real", def: 1.0, min: 0.5, max: 1.0, step: 0.05,
|
||||
group: "windows",
|
||||
label: "Fullscreen opacity",
|
||||
detail: "Applied instead of the focused opacity when a window is fullscreen",
|
||||
hypr: { path: ["decoration", "fullscreen_opacity"], option: "decoration:fullscreen_opacity", readAs: "float" }
|
||||
},
|
||||
{
|
||||
key: "roundingPower", type: "real", def: 2.0, min: 2.0, max: 10.0, step: 0.5,
|
||||
group: "windows",
|
||||
label: "Corner shape",
|
||||
detail: "2 is a circular corner; higher values approach a squircle",
|
||||
hypr: { path: ["decoration", "rounding_power"], option: "decoration:rounding_power", readAs: "float" }
|
||||
},
|
||||
|
||||
// ── Window edges ────────────────────────────────────────────────────
|
||||
// How the pointer interacts with a window's border, and how windows
|
||||
// behave near each other. All shipped by looks.lua with no way to
|
||||
// change any of it.
|
||||
{
|
||||
key: "resizeOnBorder", type: "bool", def: true, group: "edges",
|
||||
label: "Resize by dragging the border",
|
||||
detail: "Drag a window's edge to resize it, instead of only with the keyboard",
|
||||
hypr: { path: ["general", "resize_on_border"], option: "general:resize_on_border", readAs: "bool" }
|
||||
},
|
||||
{
|
||||
key: "borderGrabArea", type: "int", def: 15, min: 0, max: 40, step: 1,
|
||||
unit: "px",
|
||||
group: "edges",
|
||||
label: "Border grab area",
|
||||
detail: "How far outside the border still counts as grabbing it. Larger is easier to hit",
|
||||
hypr: { path: ["general", "extend_border_grab_area"], option: "general:extend_border_grab_area", readAs: "int" }
|
||||
},
|
||||
{
|
||||
key: "hoverIconOnBorder", type: "bool", def: true, group: "edges",
|
||||
label: "Show the resize cursor",
|
||||
detail: "Change the pointer when it is over a resizable border",
|
||||
hypr: { path: ["general", "hover_icon_on_border"], option: "general:hover_icon_on_border", readAs: "bool" }
|
||||
},
|
||||
{
|
||||
key: "snapWindowGap", type: "int", def: 10, min: 0, max: 60, step: 1,
|
||||
unit: "px",
|
||||
group: "edges",
|
||||
label: "Snap distance between windows",
|
||||
detail: "How close two floating windows must be before they snap together",
|
||||
hypr: { path: ["general", "snap", "window_gap"], option: "general:snap:window_gap", readAs: "int" }
|
||||
},
|
||||
{
|
||||
key: "snapMonitorGap", type: "int", def: 10, min: 0, max: 60, step: 1,
|
||||
unit: "px",
|
||||
group: "edges",
|
||||
label: "Snap distance to screen edges",
|
||||
detail: "How close a floating window must be to an edge before it snaps to it",
|
||||
hypr: { path: ["general", "snap", "monitor_gap"], option: "general:snap:monitor_gap", readAs: "int" }
|
||||
},
|
||||
{
|
||||
key: "snapRespectGaps", type: "bool", def: false, group: "edges",
|
||||
label: "Snapping respects gaps",
|
||||
detail: "Snapped windows keep the configured gap instead of touching",
|
||||
hypr: { path: ["general", "snap", "respect_gaps"], option: "general:snap:respect_gaps", readAs: "bool" }
|
||||
},
|
||||
|
||||
// ── Master layout ───────────────────────────────────────────────────
|
||||
// Only meaningful when the tiling layout is Master and stack. Offering
|
||||
// that layout with none of its options was an omission: it is the one
|
||||
// layout whose whole behaviour is in these settings.
|
||||
{
|
||||
key: "masterFactor", type: "real", def: 0.55, min: 0.1, max: 0.9, step: 0.05,
|
||||
group: "master",
|
||||
label: "Master area size",
|
||||
detail: "How much of the screen the master window takes",
|
||||
hypr: { path: ["master", "mfact"], option: "master:mfact", readAs: "float" }
|
||||
},
|
||||
{
|
||||
key: "masterOrientation", type: "enum", def: "left", group: "master",
|
||||
label: "Master area position",
|
||||
detail: "Which side of the screen the master window occupies",
|
||||
options: [
|
||||
{ value: "left", label: "Left" },
|
||||
{ value: "right", label: "Right" },
|
||||
{ value: "top", label: "Top" },
|
||||
{ value: "bottom", label: "Bottom" },
|
||||
{ value: "center", label: "Centre" }
|
||||
],
|
||||
hypr: { path: ["master", "orientation"], option: "master:orientation", readAs: "str" }
|
||||
},
|
||||
{
|
||||
key: "masterNewStatus", type: "enum", def: "slave", group: "master",
|
||||
label: "New windows become",
|
||||
detail: "Whether a new window takes the master area or joins the stack",
|
||||
options: [
|
||||
{ value: "master", label: "The master window" },
|
||||
{ value: "slave", label: "Part of the stack" },
|
||||
{ value: "inherit", label: "Whatever the focused window is" }
|
||||
],
|
||||
hypr: { path: ["master", "new_status"], option: "master:new_status", readAs: "str" }
|
||||
},
|
||||
{
|
||||
key: "masterNewOnTop", type: "bool", def: false, group: "master",
|
||||
label: "Add new windows at the top",
|
||||
detail: "New stack windows go above the others rather than below",
|
||||
hypr: { path: ["master", "new_on_top"], option: "master:new_on_top", readAs: "bool" }
|
||||
},
|
||||
|
||||
// ── Hyprland's own notices ──────────────────────────────────────────
|
||||
// Panama turns all four off on the user's behalf. That is a defensible
|
||||
// default and was not a decision anyone could reverse without editing
|
||||
// looks.lua, which is precisely the kind of thing this app exists to
|
||||
// stop.
|
||||
{
|
||||
key: "hyprlandLogo", type: "bool", def: false, group: "notices",
|
||||
label: "Hyprland wallpaper",
|
||||
detail: "The stock background Hyprland draws when no wallpaper is set",
|
||||
hypr: { path: ["misc", "disable_hyprland_logo"], option: "misc:disable_hyprland_logo", readAs: "bool", invert: true }
|
||||
},
|
||||
{
|
||||
key: "hyprlandSplash", type: "bool", def: false, group: "notices",
|
||||
label: "Splash text",
|
||||
detail: "The line of text Hyprland renders over the stock background",
|
||||
hypr: { path: ["misc", "disable_splash_rendering"], option: "misc:disable_splash_rendering", readAs: "bool", invert: true }
|
||||
},
|
||||
{
|
||||
key: "hyprlandUpdateNews", type: "bool", def: false, group: "notices",
|
||||
label: "Update announcements",
|
||||
detail: "The window Hyprland opens after an update to describe what changed",
|
||||
hypr: { path: ["ecosystem", "no_update_news"], option: "ecosystem:no_update_news", readAs: "bool", invert: true }
|
||||
},
|
||||
{
|
||||
key: "hyprlandDonationNag", type: "bool", def: false, group: "notices",
|
||||
label: "Donation reminders",
|
||||
detail: "The prompt Hyprland shows twice a year asking for support",
|
||||
hypr: { path: ["ecosystem", "no_donation_nag"], option: "ecosystem:no_donation_nag", readAs: "bool", invert: true }
|
||||
},
|
||||
|
||||
// ── Effects ─────────────────────────────────────────────────────────
|
||||
{
|
||||
key: "blurEnabled", type: "bool", def: true, group: "effects",
|
||||
@@ -241,6 +382,19 @@ Singleton {
|
||||
detail: "How far the shadow spreads from the window edge",
|
||||
hypr: { path: ["decoration", "shadow", "range"], option: "decoration:shadow:range", readAs: "int" }
|
||||
},
|
||||
{
|
||||
key: "shadowSharp", type: "bool", def: false, group: "effects",
|
||||
label: "Hard-edged shadow",
|
||||
detail: "A crisp shadow instead of a soft falloff",
|
||||
hypr: { path: ["decoration", "shadow", "sharp"], option: "decoration:shadow:sharp", readAs: "bool" }
|
||||
},
|
||||
{
|
||||
key: "shadowRenderPower", type: "int", def: 3, min: 1, max: 4, step: 1,
|
||||
group: "effects",
|
||||
label: "Shadow falloff",
|
||||
detail: "How sharply the shadow fades out. Higher is tighter to the window",
|
||||
hypr: { path: ["decoration", "shadow", "render_power"], option: "decoration:shadow:render_power", readAs: "int" }
|
||||
},
|
||||
{
|
||||
key: "glowEnabled", type: "bool", def: true, group: "effects",
|
||||
label: "Focus glow",
|
||||
|
||||
@@ -131,7 +131,10 @@ SettingsPage {
|
||||
SliderRow { setting: "gapsIn" }
|
||||
SliderRow { setting: "gapsOut" }
|
||||
SliderRow { setting: "borderSize"; zeroLabel: "None" }
|
||||
SliderRow { setting: "inactiveOpacity"; divider: false }
|
||||
SliderRow { setting: "roundingPower" }
|
||||
SliderRow { setting: "inactiveOpacity" }
|
||||
SliderRow { setting: "activeOpacity" }
|
||||
SliderRow { setting: "fullscreenOpacity"; divider: false }
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
@@ -143,6 +146,8 @@ SettingsPage {
|
||||
SliderRow { setting: "blurPasses" }
|
||||
ToggleRow { setting: "shadowEnabled" }
|
||||
SliderRow { setting: "shadowRange"; zeroLabel: "None" }
|
||||
SliderRow { setting: "shadowRenderPower" }
|
||||
ToggleRow { setting: "shadowSharp" }
|
||||
ToggleRow { setting: "glowEnabled" }
|
||||
SliderRow { setting: "glowRange"; zeroLabel: "None" }
|
||||
ToggleRow { setting: "animationsEnabled"; divider: false }
|
||||
|
||||
@@ -67,6 +67,45 @@ SettingsPage {
|
||||
}
|
||||
|
||||
// GNOME's Multitasking panel, in Hyprland's terms.
|
||||
// Only meaningful when the layout above is Master and stack. Hidden
|
||||
// otherwise, because a card of settings that do nothing under the layout
|
||||
// you are actually running is worse than not offering the layout at all.
|
||||
SettingsCard {
|
||||
visible: DesktopPreferences.get("windowLayout") === "master"
|
||||
title: "Master and stack"
|
||||
subtitle: "How the master area behaves. These apply only while the tiling layout above is Master and stack."
|
||||
|
||||
SliderRow { setting: "masterFactor" }
|
||||
ChoiceRow { setting: "masterOrientation" }
|
||||
ChoiceRow { setting: "masterNewStatus" }
|
||||
ToggleRow { setting: "masterNewOnTop"; divider: false }
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
title: "Window edges"
|
||||
subtitle: "How the pointer grabs a window's border, and how floating windows behave near each other and the screen edge."
|
||||
|
||||
ToggleRow { setting: "resizeOnBorder" }
|
||||
SliderRow { setting: "borderGrabArea"; zeroLabel: "Border only" }
|
||||
ToggleRow { setting: "hoverIconOnBorder" }
|
||||
SliderRow { setting: "snapWindowGap"; zeroLabel: "Touching" }
|
||||
SliderRow { setting: "snapMonitorGap"; zeroLabel: "Touching" }
|
||||
ToggleRow { setting: "snapRespectGaps"; divider: false }
|
||||
}
|
||||
|
||||
// Hyprland's own interruptions. Panama turns all four off, which is a
|
||||
// defensible default and was not previously a decision anyone could
|
||||
// reverse without editing looks.lua.
|
||||
SettingsCard {
|
||||
title: "Hyprland notices"
|
||||
subtitle: "Panama hides all of these by default. They are the compositor's own, not Panama's."
|
||||
|
||||
ToggleRow { setting: "hyprlandLogo" }
|
||||
ToggleRow { setting: "hyprlandSplash" }
|
||||
ToggleRow { setting: "hyprlandUpdateNews" }
|
||||
ToggleRow { setting: "hyprlandDonationNag"; divider: false }
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
title: "Workspaces & focus"
|
||||
subtitle: "Hyprland's workspaces are created and destroyed as you use them, so there is no fixed count to set."
|
||||
|
||||
@@ -36,6 +36,9 @@ Singleton {
|
||||
"pointer": "mouse",
|
||||
"touchpad": "mouse",
|
||||
"multitasking": "desktop",
|
||||
"edges": "desktop",
|
||||
"master": "desktop",
|
||||
"notices": "desktop",
|
||||
"weather": "appearance",
|
||||
"notifications": "notifications",
|
||||
"capture": "screen-intelligence"
|
||||
|
||||
@@ -273,6 +273,15 @@ Singleton {
|
||||
// decides, and config/dot/hypr/prefs.lua does the same conversion via
|
||||
// prefs.getInt so both sides agree.
|
||||
function hyprValue(entry: var, value: var): var {
|
||||
// Some options are phrased as a negative by the compositor -- the four
|
||||
// Hyprland notices are all `disable_x` -- while the setting reads as
|
||||
// "show x", because a switch labelled "Disable splash text" that must
|
||||
// be ON to hide something is a small cruelty. `invert` bridges the two,
|
||||
// in exactly one place, so nothing downstream has to remember which
|
||||
// options are backwards.
|
||||
if (entry.hypr.invert === true && typeof value === "boolean")
|
||||
value = !value;
|
||||
|
||||
if (typeof value === "boolean" && entry.hypr.readAs !== "bool")
|
||||
return value ? 1 : 0;
|
||||
return value;
|
||||
@@ -300,6 +309,24 @@ Singleton {
|
||||
return value ? "true" : "false";
|
||||
if (typeof value === "number")
|
||||
return String(value);
|
||||
|
||||
// A gradient is the one setting whose Lua form is not a scalar. The
|
||||
// stubs declare it as `string|{colors:string[], angle?:number}`, and
|
||||
// the string form only ever carries ONE stop -- writing
|
||||
// "rgba(a) rgba(b) 45deg" as a string is accepted and silently keeps
|
||||
// the previous value, which is how a two-stop write looks like it
|
||||
// worked and did nothing. Multi-stop must be the table form.
|
||||
if (value && typeof value === "object" && Array.isArray(value.colors)) {
|
||||
const stops = value.colors
|
||||
.map(stop => `"${String(stop).replace(/["\\]/g, "")}"`)
|
||||
.join(", ");
|
||||
const angle = Number(value.angle);
|
||||
return `{ colors = { ${stops} }` + (isFinite(angle) ? `, angle = ${angle} }` : ` }`);
|
||||
}
|
||||
|
||||
// A vec2 reaches Lua as a two-element table.
|
||||
if (Array.isArray(value) && value.length === 2)
|
||||
return `{ ${Number(value[0])}, ${Number(value[1])} }`;
|
||||
// Strings only reach here after the schema's pattern check; quoting is
|
||||
// belt-and-braces rather than the primary defence.
|
||||
return `"${String(value).replace(/["\\]/g, "")}"`;
|
||||
@@ -309,7 +336,15 @@ Singleton {
|
||||
const parts = [];
|
||||
for (const name in node) {
|
||||
const child = node[name];
|
||||
parts.push(`${name} = ${typeof child === "string" ? child : root.serialiseTable(child)}`);
|
||||
// A leaf arrives pre-serialised as a string; anything else is
|
||||
// either a nested section or a structured value (gradient, vec2)
|
||||
// that serialiseValue knows how to render.
|
||||
const rendered = typeof child === "string"
|
||||
? child
|
||||
: (Array.isArray(child) || (child && child.colors !== undefined)
|
||||
? root.serialiseValue(child)
|
||||
: root.serialiseTable(child));
|
||||
parts.push(`${name} = ${rendered}`);
|
||||
}
|
||||
return `{ ${parts.join(", ")} }`;
|
||||
}
|
||||
@@ -353,6 +388,59 @@ Singleton {
|
||||
root.drainQueue();
|
||||
}
|
||||
|
||||
// Gradients are written in one notation and read back in another, so they
|
||||
// cannot be compared directly the way every other type can.
|
||||
//
|
||||
// written: { colors = { "rgba(3b426199)" }, angle = 45 }
|
||||
// read: "993b4261 45deg"
|
||||
//
|
||||
// The stops swap to AARRGGBB order, lose their wrapper, and the angle is
|
||||
// always appended even when it was never given. Comparing the raw strings
|
||||
// reports every gradient write as rejected, which is what would have
|
||||
// happened had this been added with readAs: "str".
|
||||
function gradientMatches(expected: var, observed: string): bool {
|
||||
if (typeof observed !== "string")
|
||||
return false;
|
||||
return root.normaliseGradient(expected) === root.normaliseGradient(observed);
|
||||
}
|
||||
|
||||
// Both notations reduced to "aarrggbb aarrggbb Ndeg".
|
||||
function normaliseGradient(value: var): string {
|
||||
const stops = [];
|
||||
let angle = 0;
|
||||
|
||||
const readStop = function (text: string): void {
|
||||
const rgba = String(text).match(/rgba?\(\s*([0-9a-fA-F]{6,8})\s*\)/);
|
||||
if (rgba) {
|
||||
let hex = rgba[1].toLowerCase();
|
||||
// rgb() has no alpha; the compositor reports it as fully opaque.
|
||||
if (hex.length === 6)
|
||||
hex = hex + "ff";
|
||||
// RRGGBBAA in, AARRGGBB out.
|
||||
stops.push(hex.slice(6, 8) + hex.slice(0, 6));
|
||||
return;
|
||||
}
|
||||
const bare = String(text).match(/^([0-9a-fA-F]{8})$/);
|
||||
if (bare) {
|
||||
stops.push(bare[1].toLowerCase());
|
||||
return;
|
||||
}
|
||||
const deg = String(text).match(/^(-?[0-9.]+)deg$/);
|
||||
if (deg)
|
||||
angle = Number(deg[1]);
|
||||
};
|
||||
|
||||
if (value && typeof value === "object" && Array.isArray(value.colors)) {
|
||||
value.colors.forEach(readStop);
|
||||
if (value.angle !== undefined && isFinite(Number(value.angle)))
|
||||
angle = Number(value.angle);
|
||||
} else {
|
||||
String(value).trim().split(/\s+/).forEach(readStop);
|
||||
}
|
||||
|
||||
return stops.join(" ") + " " + angle + "deg";
|
||||
}
|
||||
|
||||
function matchesObserved(entry: var, value: var, answer: var): bool {
|
||||
if (!answer)
|
||||
return false;
|
||||
@@ -370,6 +458,12 @@ Singleton {
|
||||
case "css":
|
||||
// Gaps read back as a box, e.g. "10 10 10 10".
|
||||
return Number(String(answer.css).trim().split(/\s+/)[0]) === expected;
|
||||
case "gradient":
|
||||
return root.gradientMatches(expected, answer.gradient);
|
||||
case "vec2":
|
||||
return Array.isArray(answer.vec2) && Array.isArray(expected)
|
||||
&& Number(answer.vec2[0]) === Number(expected[0])
|
||||
&& Number(answer.vec2[1]) === Number(expected[1]);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user