continuing dev

This commit is contained in:
Noah Masur
2025-01-29 21:12:48 -05:00
parent c7933f8502
commit 0ebd0bac2c
55 changed files with 362 additions and 347 deletions

View File

@ -0,0 +1,38 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.nmasur.presets.fonts;
in
{
options.nmasur.presets.fonts.enable = lib.mkEnableOption "Font configuration";
config = lib.mkIf cfg.enable {
home.packages = [
pkgs.victor-mono # Used for Vim and Terminal
pkgs.nerd-fonts.hack # For Polybar, Rofi
];
fonts.fontconfig = {
enable = true;
defaultFonts.monospace = [ "Victor Mono" ];
};
xsession.windowManager.i3.config.fonts = {
names = [ "pango:Victor Mono" ];
# style = "Regular";
# size = 11.0;
};
services.polybar.config."bar/main".font-0 = "Hack Nerd Font:size=10;2";
programs.rofi.font = "Hack Nerd Font 14";
programs.alacritty.settings.font.normal.family = "VictorMono";
programs.kitty.font.name = "VictorMono Nerd Font Mono";
config.nmasur.presets.programs.wezterm.font = "VictorMono Nerd Font Mono";
services.dunst.settings.global.font = "Hack Nerd Font 14";
};
}

View File

@ -0,0 +1,32 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.nmasur.presets.programs.cargo;
in
{
options.nmasur.presets.programs.cargo.enable = lib.mkEnableOption "Cargo for programming language.";
config = lib.mkIf cfg.enable {
programs.fish.shellAbbrs = {
ca = "cargo";
};
home.packages = with pkgs; [
gcc
rustc
cargo
cargo-watch
clippy
rustfmt
pkg-config
openssl
];
};
}

View File

@ -0,0 +1,16 @@
{ config, lib, ... }:
let
cfg = config.nmasur.presets.programs.haskell;
in
{
options.nmasur.presets.programs.haskell.enable =
lib.mkEnableOption "Haskell programming language config.";
config = lib.mkIf cfg.enable {
# Binary Cache for Haskell.nix
nix.settings.trusted-public-keys = [ "hydra.iohk.io:f/Ea+s+dFdN+3Y/G+FDgSq+a5NEWhJGzdjvKNGv0/EQ=" ];
nix.settings.substituters = [ "https://cache.iog.io" ];
};
}

View File

@ -0,0 +1,21 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.nmasur.presets.programs.lua;
in
{
options.nmasur.presets.programs.lua.enable = lib.mkEnableOption "Lua programming language.";
config = lib.mkIf cfg.enable {
home.packages = [
pkgs.stylua # Lua formatter
pkgs.sumneko-lua-language-server # Lua LSP
];
};
}

View File

@ -0,0 +1,27 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.nmasur.presets.programs.python;
in
{
options.nmasur.presets.programs.python.enable = lib.mkEnableOption "Python programming language.";
config = lib.mkIf cfg.enable {
home.packages = [
pkgs.pyright # Python language server
pkgs.black # Python formatter
pkgs.python310Packages.flake8 # Python linter
];
programs.fish.shellAbbrs = {
py = "python3";
};
};
}

View File

@ -0,0 +1,50 @@
{ config, pkgs, ... }:
let
rofi = config.home-manager.users.${config.user}.programs.rofi.finalPackage;
in
{
# Adapted from:
# A rofi powered menu to execute brightness choices.
config.brightnessCommand = builtins.toString (
pkgs.writeShellScript "brightness" ''
dimmer="󰃝"
medium="󰃟"
brighter="󰃠"
chosen=$(printf '%s;%s;%s\n' \
"$dimmer" \
"$medium" \
"$brighter" \
| ${rofi}/bin/rofi \
-theme-str '@import "brightness.rasi"' \
-hover-select \
-me-select-entry ''' \
-me-accept-entry MousePrimary \
-dmenu \
-sep ';' \
-selected-row 1)
case "$chosen" in
"$dimmer")
${pkgs.ddcutil}/bin/ddcutil --display 1 setvcp 10 25; ${pkgs.ddcutil}/bin/ddcutil --disable-dynamic-sleep --display 2 setvcp 10 25
;;
"$medium")
${pkgs.ddcutil}/bin/ddcutil --display 1 setvcp 10 75; ${pkgs.ddcutil}/bin/ddcutil --disable-dynamic-sleep --display 2 setvcp 10 75
;;
"$brighter")
${pkgs.ddcutil}/bin/ddcutil --display 1 setvcp 10 100; ${pkgs.ddcutil}/bin/ddcutil --disable-dynamic-sleep --display 2 setvcp 10 100
;;
*) exit 1 ;;
esac
''
);
}

View File

@ -168,7 +168,7 @@ in
home.file.".local/share/rofi/themes" = {
recursive = true;
source = ./rofi/themes;
source = ./themes;
};
};

View File

@ -0,0 +1,64 @@
{ config, pkgs, ... }:
let
rofi = config.home-manager.users.${config.user}.programs.rofi.finalPackage;
in
{
# Adapted from:
# https://gitlab.com/vahnrr/rofi-menus/-/blob/b1f0e8a676eda5552e27ef631b0d43e660b23b8e/scripts/rofi-power
# A rofi powered menu to execute power related action.
config.powerCommand = builtins.toString (
pkgs.writeShellScript "powermenu" ''
power_off=''
reboot=''
lock=''
suspend='󰒲'
log_out=''
chosen=$(printf '%s;%s;%s;%s;%s\n' \
"$power_off" \
"$reboot" \
"$lock" \
"$suspend" \
"$log_out" \
| ${rofi}/bin/rofi \
-theme-str '@import "power.rasi"' \
-hover-select \
-me-select-entry "" \
-me-accept-entry MousePrimary \
-dmenu \
-sep ';' \
-selected-row 2)
confirm () {
${builtins.readFile ./rofi-prompt.sh}
}
case "$chosen" in
"$power_off")
confirm 'Shutdown?' && doas shutdown now
;;
"$reboot")
confirm 'Reboot?' && doas reboot
;;
"$lock")
${pkgs.betterlockscreen}/bin/betterlockscreen --lock --display 1 --blur 0.5 --span
;;
"$suspend")
systemctl suspend
;;
"$log_out")
confirm 'Logout?' && i3-msg exit
;;
*) exit 1 ;;
esac
''
);
}

View File

@ -0,0 +1,22 @@
#!/usr/bin/env bash
# Credit: https://gist.github.com/Nervengift/844a597104631c36513c
sink=$(
ponymix -t sink list |
awk '/^sink/ {s=$1" "$2;getline;gsub(/^ +/,"",$0);print s" "$0}' |
rofi \
-dmenu \
-p 'pulseaudio sink:' \
-width 100 \
-hover-select \
-me-select-entry '' \
-me-accept-entry MousePrimary \
-theme-str 'inputbar { enabled: false; }' |
grep -Po '[0-9]+(?=:)'
) &&
ponymix set-default -d "$sink" &&
for input in $(ponymix list -t sink-input | grep -Po '[0-9]+(?=:)'); do
echo "$input -> $sink"
ponymix -t sink-input -d "$input" move "$sink"
done

View File

@ -0,0 +1,47 @@
#!/usr/bin/env sh
# Credit: https://gitlab.com/vahnrr/rofi-menus/-/blob/b1f0e8a676eda5552e27ef631b0d43e660b23b8e/scripts/rofi-prompt
# Rofi powered menu to prompt a message and get a yes/no answer.
# Uses: rofi
yes='Confirm'
no='Cancel'
query='Are you sure?'
while [ $# -ne 0 ]; do
case "$1" in
-y | --yes)
[ -n "$2" ] && yes="$2" || yes=''
shift
;;
-n | --no)
[ -n "$2" ] && no="$2" || no=''
shift
;;
-q | --query)
[ -n "$2" ] && query="$2"
shift
;;
esac
shift
done
chosen=$(printf '%s;%s\n' "$yes" "$no" |
rofi -theme-str '@import "prompt.rasi"' \
-hover-select \
-me-select-entry "" \
-me-accept-entry MousePrimary \
-p "$query" \
-dmenu \
-sep ';' \
-a 0 \
-u 1 \
-selected-row 1)
case "$chosen" in
"$yes") return 0 ;;
*) return 1 ;;
esac

View File

@ -0,0 +1,6 @@
@import "common.rasi"
#window {
width: 605px;
height: 230px;
}

View File

@ -0,0 +1,57 @@
/**
* Allows to change the settings of every menu simply by editing this file
* https://gitlab.com/vahnrr/rofi-menus/-/blob/b1f0e8a676eda5552e27ef631b0d43e660b23b8e/themes/shared/settings.rasi
*/
* {
/* General */
font: "Hack Nerd Font 60";
/* option menus: i3-layout, music, power and screenshot
*
* Values bellow are 'no-padding' ones for a size 60 (@icon-font) font, played
* around using this character: ■
* We then add add 100 actual padding around the icons.
* -12px 0px -19px -96px */
option-element-padding: 1% 1% 1% 1%;
option-5-window-padding: 4% 4%;
option-5-listview-spacing: 15px;
prompt-text-font: "Hack Nerd Font 18";
prompt-window-height: 300px;
prompt-window-width: 627px;
prompt-window-border: 2px;
prompt-prompt-padding: 20px 30px;
prompt-prompt-margin: 8px;
prompt-listview-padding: 60px 114px 0px 114px;
/* Values bellow are 'no-padding' ones for a size 18 (@prompt-text-font) font,
* played around using this character: ■
* We then add add 30 actual padding around the text.
* -4px -1px -6px -28px */
prompt-element-padding: 26px 29px 24px 2px;
vpn-textbox-prompt-colon-padding: @network-textbox-prompt-colon-padding;
}
/**
* Settings used in every rofi option menu:
*/
#window {
children: [ horibox ];
}
#horibox {
children: [ listview ];
}
#listview {
layout: horizontal;
}
element {
padding: 40px 68px 43px 30px;
}
#window {
padding: 20px;
}
#listview {
spacing: 10px;
lines: 5;
}

View File

@ -0,0 +1,3 @@
#entry {
placeholder: "Launch Program";
}

View File

@ -0,0 +1,6 @@
@import "common.rasi"
#window {
width: 980px;
height: 230px;
}

View File

@ -0,0 +1,30 @@
/**
* This theme is intended for a 2 items option menu with a headerbar.
* https://gitlab.com/vahnrr/rofi-menus/-/blob/b1f0e8a676eda5552e27ef631b0d43e660b23b8e/themes/prompt.rasi
*/
@import "common.rasi"
* {
font: @prompt-text-font;
}
#window {
height: @prompt-window-height;
width: @prompt-window-width;
children: [ inputbar, horibox ];
border: @prompt-window-border;
}
#inputbar {
enabled: false;
}
#prompt {
padding: @prompt-prompt-padding;
margin: @prompt-prompt-margin;
}
#listview {
padding: @prompt-listview-padding;
spacing: @option-5-listview-spacing;
lines: 2;
}
#element {
font: @prompt-text-font;
padding: @prompt-element-padding;
}

View File

@ -0,0 +1,30 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.nmasur.presets.programs.terraform;
in
{
options.nmasur.presets.programs.terraform.enable =
lib.mkEnableOption "Terraform infrastructure management";
config = lib.mkIf cfg.enable {
unfreePackages = [ "terraform" ];
programs.fish.shellAbbrs = {
te = "terraform";
};
home.packages = with pkgs; [
terraform
terraform-ls
tflint
];
};
}

View File

@ -7,12 +7,17 @@
let
cfg = config.nmasur.presets.programs.wezterm;
font = config.programs.kitty.font.name;
in
{
options.nmasur.presets.programs.wezterm.enable = lib.mkEnableOption "WezTerm terminal";
options.nmasur.presets.programs.wezterm = {
enable = lib.mkEnableOption "WezTerm terminal";
font = lib.mkOption {
type = lib.types.str;
description = "Name of the font for WezTerm";
};
};
config = lib.mkIf cfg.enable {
# Set the i3 terminal
@ -99,7 +104,7 @@ in
bottom = 12,
}
config.font = wezterm.font('${font}', { weight = 'Bold'})
config.font = wezterm.font('${cfg.font}', { weight = 'Bold'})
config.font_size = ${if pkgs.stdenv.isLinux then "14.0" else "18.0"}
-- Fix color blocks instead of text
@ -108,7 +113,7 @@ in
-- Tab Bar
config.hide_tab_bar_if_only_one_tab = true
config.window_frame = {
font = wezterm.font('${font}', { weight = 'Bold'}),
font = wezterm.font('${cfg.font}', { weight = 'Bold'}),
font_size = ${if pkgs.stdenv.isLinux then "12.0" else "16.0"},
}

View File

@ -0,0 +1,25 @@
{ config, lib, ... }:
let
cfg = config.nmasur.presets.programs.wine;
in
{
options.nmasur.presets.programs.wine.enable = lib.mkEnableOption "Wine settings";
config = lib.mkIf cfg.enable {
# Ignore wine directories in searches
home.file =
let
ignorePatterns = ''
.wine/
drive_c/'';
in
{
".rgignore".text = ignorePatterns;
".fdignore".text = ignorePatterns;
};
};
}

View File

@ -0,0 +1 @@
indent_type = "Spaces"

View File

@ -0,0 +1,112 @@
--- === ControlEscape ===
---
--- Adapted very loosely from https://github.com/jasonrudolph/ControlEscape.spoon
--- Removed timing/delay; always send Escape as well as Control
---
--- Make the `control` key more useful: If the `control` key is tapped, treat it
--- as the `escape` key. If the `control` key is held down and used in
--- combination with another key, then provide the normal `control` key
--- behavior.
local obj = {}
obj.__index = obj
-- Metadata
obj.name = "ControlEscape"
obj.version = "0.1"
obj.author = "Jason Rudolph <jason@jasonrudolph.com>"
obj.homepage = "https://github.com/jasonrudolph/ControlEscape.spoon"
obj.license = "MIT - https://opensource.org/licenses/MIT"
function obj:init()
self.movements = 0
self.sendEscape = false
self.lastModifiers = {}
-- Create an eventtap to run each time the modifier keys change (i.e., each
-- time a key like control, shift, option, or command is pressed or released)
self.controlTap = hs.eventtap.new({ hs.eventtap.event.types.flagsChanged }, function(event)
local newModifiers = event:getFlags()
-- If this change to the modifier keys does not involve a *change* to the
-- up/down state of the `control` key (i.e., it was up before and it's
-- still up, or it was down before and it's still down), then don't take
-- any action.
if self.lastModifiers["ctrl"] == newModifiers["ctrl"] then
return false
end
-- Control was not down but is now
if not self.lastModifiers["ctrl"] then
-- Only prepare to send escape if no other modifier keys are in use
self.lastModifiers = newModifiers
if not self.lastModifiers["cmd"] and not self.lastModifiers["alt"] then
self.sendEscape = true
self.movements = 0
end
-- Control was down and is up, hasn't been blocked by another key, and
-- isn't above the movement threshold
elseif self.sendEscape == true and not newModifiers["ctrl"] and self.movements < 30 then
self.lastModifiers = newModifiers
-- Allow for shift-escape
if newModifiers["shift"] then
hs.eventtap.keyStroke({ "shift" }, "escape", 0)
else
hs.eventtap.keyStroke(newModifiers, "escape", 0)
end
self.sendEscape = false
self.movements = 0
self.numberOfCharacters = 0
-- Control was down and is up, but isn't ready to send escape
else
self.lastModifiers = newModifiers
end
end)
-- If any other key is pressed, don't send escape
self.asModifier = hs.eventtap.new({ hs.eventtap.event.types.keyDown }, function(_)
self.sendEscape = false
-- print("Don't sent escape")
end)
-- If mouse is moving significantly, don't send escape
self.scrolling = hs.eventtap.new({ hs.eventtap.event.types.gesture }, function(event)
local touches = event:getTouches()
local i, v = next(touches, nil)
while i do
if v["phase"] == "moved" then
-- Increment the movement counter
self.movements = self.movements + 1
end
i, v = next(touches, i) -- get next index
end
end)
end
--- ControlEscape:start()
--- Method
--- Start sending `escape` when `control` is pressed and released in isolation
function obj:start()
self.controlTap:start()
self.asModifier:start()
self.scrolling:start()
end
--- ControlEscape:stop()
--- Method
--- Stop sending `escape` when `control` is pressed and released in isolation
function obj:stop()
-- Stop monitoring keystrokes
self.controlTap:stop()
self.asModifier:stop()
self.scrolling:stop()
-- Reset state
self.sendEscape = false
self.lastModifiers = {}
end
return obj

View File

@ -0,0 +1,21 @@
# Credit: https://github.com/Ptujec/LaunchBar/blob/f7b5a0dba9919c2fec879513f68a044f78748539/Notifications/Dismiss%20all%20notifications.lbaction/Contents/Scripts/default.applescript
tell application "System Events"
try
set _groups to groups of UI element 1 of scroll area 1 of group 1 of window "Notification Center" of application process "NotificationCenter"
repeat with _group in _groups
set _actions to actions of _group
repeat with _action in _actions
if description of _action is in {"Schlie§en", "Alle entfernen", "Close", "Clear All"} then
perform _action
end if
end repeat
end repeat
end try
end tell

View File

@ -0,0 +1,298 @@
/* Credit: https://gist.github.com/lancethomps/a5ac103f334b171f70ce2ff983220b4f */
function run(input, parameters) {
const appNames = [];
const skipAppNames = [];
const verbose = true;
const scriptName = "close_notifications_applescript";
const CLEAR_ALL_ACTION = "Clear All";
const CLEAR_ALL_ACTION_TOP = "Clear";
const CLOSE_ACTION = "Close";
const notNull = (val) => {
return val !== null && val !== undefined;
};
const isNull = (val) => {
return !notNull(val);
};
const notNullOrEmpty = (val) => {
return notNull(val) && val.length > 0;
};
const isNullOrEmpty = (val) => {
return !notNullOrEmpty(val);
};
const isError = (maybeErr) => {
return notNull(maybeErr) && (maybeErr instanceof Error || maybeErr.message);
};
const systemVersion = () => {
return Application("Finder").version().split(".").map(val => parseInt(val));
};
const systemVersionGreaterThanOrEqualTo = (vers) => {
return systemVersion()[0] >= vers;
};
const isBigSurOrGreater = () => {
return systemVersionGreaterThanOrEqualTo(11);
};
const V11_OR_GREATER = isBigSurOrGreater();
const APP_NAME_MATCHER_ROLE = V11_OR_GREATER ? "AXStaticText" : "AXImage";
const hasAppNames = notNullOrEmpty(appNames);
const hasSkipAppNames = notNullOrEmpty(skipAppNames);
const hasAppNameFilters = hasAppNames || hasSkipAppNames;
const appNameForLog = hasAppNames ? ` [${appNames.join(",")}]` : "";
const logs = [];
const log = (message, ...optionalParams) => {
let message_with_prefix = `${new Date().toISOString().replace("Z", "").replace("T", " ")} [${scriptName}]${appNameForLog} ${message}`;
console.log(message_with_prefix, optionalParams);
logs.push(message_with_prefix);
};
const logError = (message, ...optionalParams) => {
if (isError(message)) {
let err = message;
message = `${err}${err.stack ? (" " + err.stack) : ""}`;
}
log(`ERROR ${message}`, optionalParams);
};
const logErrorVerbose = (message, ...optionalParams) => {
if (verbose) {
logError(message, optionalParams);
}
};
const logVerbose = (message) => {
if (verbose) {
log(message);
}
};
const getLogLines = () => {
return logs.join("\n");
};
const getSystemEvents = () => {
let systemEvents = Application("System Events");
systemEvents.includeStandardAdditions = true;
return systemEvents;
};
const getNotificationCenter = () => {
try {
return getSystemEvents().processes.byName("NotificationCenter");
} catch (err) {
logError("Could not get NotificationCenter");
throw err;
}
};
const getNotificationCenterGroups = (retryOnError = false) => {
try {
let notificationCenter = getNotificationCenter();
if (notificationCenter.windows.length <= 0) {
return [];
}
if (!V11_OR_GREATER) {
return notificationCenter.windows();
}
return notificationCenter.windows[0].uiElements[0].uiElements[0].uiElements();
} catch (err) {
logError("Could not get NotificationCenter groups");
if (retryOnError) {
logError(err);
log("Retrying getNotificationCenterGroups...");
return getNotificationCenterGroups(false);
} else {
throw err;
}
}
};
const isClearButton = (description, name) => {
return description === "button" && name === CLEAR_ALL_ACTION_TOP;
};
const matchesAnyAppNames = (value, checkValues) => {
if (isNullOrEmpty(checkValues)) {
return false;
}
let lowerAppName = value.toLowerCase();
for (let checkValue of checkValues) {
if (lowerAppName === checkValue.toLowerCase()) {
return true;
}
}
return false;
};
const matchesAppName = (role, value) => {
if (role !== APP_NAME_MATCHER_ROLE) {
return false;
}
if (hasAppNames) {
return matchesAnyAppNames(value, appNames);
}
return !matchesAnyAppNames(value, skipAppNames);
};
const notificationGroupMatches = (group) => {
try {
let description = group.description();
if (V11_OR_GREATER && isClearButton(description, group.name())) {
return true;
}
if (V11_OR_GREATER && description !== "group") {
return false;
}
if (!V11_OR_GREATER) {
let matchedAppName = !hasAppNameFilters;
if (!matchedAppName) {
for (let elem of group.uiElements()) {
if (matchesAppName(elem.role(), elem.description())) {
matchedAppName = true;
break;
}
}
}
if (matchedAppName) {
return notNull(findCloseActionV10(group, -1));
}
return false;
}
if (!hasAppNameFilters) {
return true;
}
let firstElem = group.uiElements[0];
return matchesAppName(firstElem.role(), firstElem.value());
} catch (err) {
logErrorVerbose(`Caught error while checking window, window is probably closed: ${err}`);
logErrorVerbose(err);
}
return false;
};
const findCloseActionV10 = (group, closedCount) => {
try {
for (let elem of group.uiElements()) {
if (elem.role() === "AXButton" && elem.title() === CLOSE_ACTION) {
return elem.actions["AXPress"];
}
}
} catch (err) {
logErrorVerbose(`(group_${closedCount}) Caught error while searching for close action, window is probably closed: ${err}`);
logErrorVerbose(err);
return null;
}
log("No close action found for notification");
return null;
};
const findCloseAction = (group, closedCount) => {
try {
if (!V11_OR_GREATER) {
return findCloseActionV10(group, closedCount);
}
let checkForPress = isClearButton(group.description(), group.name());
let clearAllAction;
let closeAction;
for (let action of group.actions()) {
let description = action.description();
if (description === CLEAR_ALL_ACTION) {
clearAllAction = action;
break;
} else if (description === CLOSE_ACTION) {
closeAction = action;
} else if (checkForPress && description === "press") {
clearAllAction = action;
break;
}
}
if (notNull(clearAllAction)) {
return clearAllAction;
} else if (notNull(closeAction)) {
return closeAction;
}
} catch (err) {
logErrorVerbose(`(group_${closedCount}) Caught error while searching for close action, window is probably closed: ${err}`);
logErrorVerbose(err);
return null;
}
log("No close action found for notification");
return null;
};
const closeNextGroup = (groups, closedCount) => {
try {
for (let group of groups) {
if (notificationGroupMatches(group)) {
let closeAction = findCloseAction(group, closedCount);
if (notNull(closeAction)) {
try {
closeAction.perform();
return [true, 1];
} catch (err) {
logErrorVerbose(`(group_${closedCount}) Caught error while performing close action, window is probably closed: ${err}`);
logErrorVerbose(err);
}
}
return [true, 0];
}
}
return false;
} catch (err) {
logError("Could not run closeNextGroup");
throw err;
}
};
try {
let groupsCount = getNotificationCenterGroups(true).filter(group => notificationGroupMatches(group)).length;
if (groupsCount > 0) {
logVerbose(`Closing ${groupsCount}${appNameForLog} notification group${(groupsCount > 1 ? "s" : "")}`);
let startTime = new Date().getTime();
let closedCount = 0;
let maybeMore = true;
let maxAttempts = 2;
let attempts = 1;
while (maybeMore && ((new Date().getTime() - startTime) <= (1000 * 30))) {
try {
let closeResult = closeNextGroup(getNotificationCenterGroups(), closedCount);
maybeMore = closeResult[0];
if (maybeMore) {
closedCount = closedCount + closeResult[1];
}
} catch (innerErr) {
if (maybeMore && closedCount === 0 && attempts < maxAttempts) {
log(`Caught an error before anything closed, trying ${maxAttempts - attempts} more time(s).`)
attempts++;
} else {
throw innerErr;
}
}
}
} else {
throw Error(`No${appNameForLog} notifications found...`);
}
} catch (err) {
logError(err);
logError(err.message);
getLogLines();
throw err;
}
return getLogLines();
}

View File

@ -0,0 +1,17 @@
--- === Dismiss Alerts ===
local obj = {}
obj.__index = obj
-- Metadata
obj.name = "DismissAlerts"
obj.version = "0.1"
obj.license = "MIT - https://opensource.org/licenses/MIT"
function obj:init()
hs.hotkey.bind({ "cmd", "alt", "ctrl" }, "k", function()
hs.osascript.applescriptFromFile("Spoons/DismissAlerts.spoon/close_notifications.applescript")
end)
end
return obj

View File

@ -0,0 +1,111 @@
--- === Launcher ===
local obj = {}
obj.__index = obj
-- Metadata
obj.name = "Launcher"
obj.version = "0.1"
obj.license = "MIT - https://opensource.org/licenses/MIT"
local screen = hs.screen.primaryScreen()
local switcherWidth = 500
function obj:init()
-- Begin launcher mode
if self.launcher == nil then
self.launcher = hs.hotkey.modal.new("ctrl", "space")
print(self.canvas)
print(obj.canvas)
end
-- Behaviors on enter
function self.launcher:entered()
-- hs.alert("Entered mode")
obj.canvas = hs.canvas.new({
x = (screen:fullFrame().x + screen:fullFrame().w) / 2 - switcherWidth / 2,
y = 1,
h = 3,
w = switcherWidth,
})
-- Draw switcher
obj.canvas[#obj.canvas + 1] = {
action = "build",
type = "rectangle",
}
obj.canvas[#obj.canvas + 1] = {
type = "rectangle",
fillColor = { alpha = 1, red = 0.8, green = 0.6, blue = 0.3 },
action = "fill",
}
obj.canvas:show()
end
-- Behaviors on exit
function self.launcher:exited()
-- hs.alert("Exited mode")
obj.canvas:delete(0.2)
end
-- Use escape to exit launcher mode
self.launcher:bind("", "escape", function()
self.launcher:exit()
end)
-- Launcher shortcuts
self.launcher:bind("ctrl", "space", function() end)
self.launcher:bind("", "return", function()
self:switch("@wezterm@")
end)
self.launcher:bind("", "C", function()
self:switch("Calendar.app")
end)
self.launcher:bind("shift", "D", function()
hs.execute("launchctl remove com.paloaltonetworks.gp.pangps")
hs.execute("launchctl remove com.paloaltonetworks.gp.pangpa")
hs.alert.show("Disconnected from GlobalProtect", nil, nil, 4)
self.launcher:exit()
end)
self.launcher:bind("", "E", function()
self:switch("Mail.app")
end)
self.launcher:bind("", "F", function()
self:switch("@firefox@")
end)
self.launcher:bind("", "H", function()
self:switch("Hammerspoon.app")
end)
self.launcher:bind("", "M", function()
self:switch("Messages.app")
end)
self.launcher:bind("", "O", function()
self:switch("@obsidian@")
end)
self.launcher:bind("", "P", function()
self:switch("System Preferences.app")
end)
self.launcher:bind("shift", "P", function()
hs.execute("launchctl load /Library/LaunchAgents/com.paloaltonetworks.gp.pangps.plist")
hs.execute("launchctl load /Library/LaunchAgents/com.paloaltonetworks.gp.pangpa.plist")
hs.alert.show("Reconnecting to GlobalProtect", nil, nil, 4)
self.launcher:exit()
end)
self.launcher:bind("", "R", function()
hs.console.clearConsole()
hs.reload()
end)
self.launcher:bind("", "S", function()
self:switch("@slack@")
end)
self.launcher:bind("", "Z", function()
self:switch("zoom.us.app")
end)
end
function obj:switch(app)
hs.application.launchOrFocus(app)
self.launcher:exit()
end
return obj

View File

@ -0,0 +1,81 @@
--- === Move Window ===
local obj = {}
obj.__index = obj
-- Metadata
obj.name = "MoveWindow"
obj.version = "0.1"
obj.license = "MIT - https://opensource.org/licenses/MIT"
function obj:init()
hs.window.animationDuration = 0.1
dofile(hs.spoons.resourcePath("worklayout.lua"))()
-- bind hotkey
hs.hotkey.bind({ "alt", "ctrl", "cmd" }, "n", function()
-- get the focused window
local win = hs.window.focusedWindow()
-- get the screen where the focused window is displayed, a.k.a. current screen
local screen = win:screen()
-- local nextScreen = screen:next()
-- compute the unitRect of the focused window relative to the current screen
-- and move the window to the next screen setting the same unitRect
win:moveToScreen(screen:next(), true, true, 0)
end)
hs.hotkey.bind({ "alt", "ctrl", "cmd" }, "b", function()
local win = hs.window.focusedWindow()
local screen = win:screen()
win:moveToScreen(screen:previous(), true, true, 0)
end)
-- Maximize
hs.hotkey.bind({ "alt", "ctrl", "cmd" }, "m", function()
-- get the focused window
local win = hs.window.focusedWindow()
local frame = win:frame()
-- maximize if possible
local max = win:screen():fullFrame()
frame.x = max.x
frame.y = max.y
frame.w = max.w
frame.h = max.h
win:setFrame(frame)
-- -- first maximize to grid
-- hs.grid.maximizeWindow(win)
-- -- then spam maximize
-- for i = 1, 8 do
-- win:maximize()
-- end
end)
-- Half-maximize (right)
hs.hotkey.bind({ "alt", "ctrl", "cmd" }, "o", function()
-- get the focused window
local win = hs.window.focusedWindow()
local frame = win:frame()
-- maximize if possible
local max = win:screen():fullFrame()
frame.x = (max.x * 2 + max.w) / 2
frame.y = max.y
frame.w = max.w / 2
frame.h = max.h
win:setFrame(frame)
end)
-- Half-maximize (left)
hs.hotkey.bind({ "alt", "ctrl", "cmd" }, "u", function()
-- get the focused window
local win = hs.window.focusedWindow()
local frame = win:frame()
-- maximize if possible
local max = win:screen():fullFrame()
frame.x = max.x
frame.y = max.y
frame.w = max.w / 2
frame.h = max.h
win:setFrame(frame)
end)
end
return obj

View File

@ -0,0 +1,70 @@
--- === Work Layout ===
-- Portions of this is adopted from:
-- https://github.com/anishathalye/dotfiles-local/tree/ffdadd313e58514eb622736b09b91a7d7eb7c6c9/hammerspoon
-- License is also available:
-- https://github.com/anishathalye/dotfiles-local/blob/ffdadd313e58514eb622736b09b91a7d7eb7c6c9/LICENSE.md
WORK_ONLY_MONITOR = "DELL U4021QW"
LAPTOP_MONITOR = "Built-in Retina Display"
-- Used to find out the name of the monitor in Hammerspoon
local function dump(o)
if type(o) == "table" then
local s = "{ "
for k, v in pairs(o) do
if type(k) ~= "number" then
k = '"' .. k .. '"'
end
s = s .. "[" .. k .. "] = " .. dump(v) .. ","
end
return s .. "} "
else
return tostring(o)
end
end
-- Turn on when looking for the monitor name
print(dump(hs.screen.allScreens()))
local function concat(...)
local res = {}
for _, tab in ipairs({ ... }) do
for _, elem in ipairs(tab) do
table.insert(res, elem)
end
end
return res
end
local function worklayout()
hs.hotkey.bind({ "alt", "ctrl", "cmd" }, "l", function()
local u = hs.geometry.unitrect
-- set the layout
local left = {
{ "WezTerm", nil, WORK_ONLY_MONITOR, u(0, 0, 1 / 2, 1), nil, nil, visible = true },
}
local right = {
{ "Slack", nil, WORK_ONLY_MONITOR, u(1 / 2, 0, 1 / 2, 1), nil, nil, visible = true },
{ "Mail", nil, WORK_ONLY_MONITOR, u(1 / 2, 0, 1 / 2, 1), nil, nil, visible = true },
{ "zoom.us", nil, WORK_ONLY_MONITOR, u(5 / 8, 1 / 4, 1 / 4, 1 / 2), nil, nil, visible = true },
}
local laptop = {
{ "Firefox", nil, LAPTOP_MONITOR, u(0, 0, 1, 1), nil, nil, visible = true },
{ "Obsidian", nil, LAPTOP_MONITOR, u(0, 0, 1, 1), nil, nil, visible = true },
{ "Calendar", nil, LAPTOP_MONITOR, u(0, 0, 1, 1), nil, nil, visible = true },
}
local layout = concat(left, right, laptop)
hs.layout.apply(layout)
end)
-- Reload Hammerspoon whenever layout changes
hs.screen.watcher.new(function()
-- Pause for 5 seconds to give time for layout to change
hs.timer.doAfter(5, function()
-- Perform the actual reload
hs.reload()
end)
end)
end
return worklayout

View File

@ -15,20 +15,18 @@ in
lib.mkEnableOption "Hammerspoon macOS automation";
config = lib.mkIf cfg.enable {
xdg.configFile."hammerspoon/init.lua".source = ./hammerspoon/init.lua;
xdg.configFile."hammerspoon/Spoons/ControlEscape.spoon".source =
./hammerspoon/Spoons/ControlEscape.spoon;
xdg.configFile."hammerspoon/Spoons/DismissAlerts.spoon".source =
./hammerspoon/Spoons/DismissAlerts.spoon;
xdg.configFile."hammerspoon/init.lua".source = ./init.lua;
xdg.configFile."hammerspoon/Spoons/ControlEscape.spoon".source = ./Spoons/ControlEscape.spoon;
xdg.configFile."hammerspoon/Spoons/DismissAlerts.spoon".source = ./Spoons/DismissAlerts.spoon;
xdg.configFile."hammerspoon/Spoons/Launcher.spoon/init.lua".source = pkgs.substituteAll {
src = ./hammerspoon/Spoons/Launcher.spoon/init.lua;
src = ./Spoons/Launcher.spoon/init.lua;
firefox = "${pkgs.firefox-bin}/Applications/Firefox.app";
discord = "${pkgs.discord}/Applications/Discord.app";
wezterm = "${pkgs.wezterm}/Applications/WezTerm.app";
obsidian = "${pkgs.obsidian}/Applications/Obsidian.app";
slack = "${pkgs.slack}/Applications/Slack.app";
};
xdg.configFile."hammerspoon/Spoons/MoveWindow.spoon".source = ./hammerspoon/Spoons/MoveWindow.spoon;
xdg.configFile."hammerspoon/Spoons/MoveWindow.spoon".source = ./Spoons/MoveWindow.spoon;
home.activation.reloadHammerspoon =
config.home-manager.users.${config.user}.lib.dag.entryAfter [ "writeBoundary" ]

View File

@ -0,0 +1,5 @@
hs.ipc.cliInstall() -- Install Hammerspoon CLI program
hs.loadSpoon("ControlEscape"):start() -- Load Hammerspoon bits from https://github.com/jasonrudolph/ControlEscape.spoon
hs.loadSpoon("Launcher"):init()
hs.loadSpoon("DismissAlerts"):init()
hs.loadSpoon("MoveWindow"):init()

View File

@ -0,0 +1,33 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.nmasur.presets.services.loadkey;
in
{
options.nmasur.presets.services.loadkey.enable =
lib.mkEnableOption "Load the private key as an SSH file";
config = lib.mkIf cfg.enable {
home.activation = {
# Always load the key if it doesn't exist
cloneDotfiles = config.lib.dag.entryAfter [ "writeBoundary" ] ''
if [ ! -f ~/.ssh/id_ed25519 ]; then
run mkdir -p ~/.ssh/
$DRY_RUN_CMD mkdir --parents $VERBOSE_ARG $(dirname "${config.dotfilesPath}")
$DRY_RUN_CMD ${pkgs.git}/bin/git \
clone ${config.dotfilesRepo} "${config.dotfilesPath}"
fi
'';
};
};
}