From 3d676babd51a4bb62d5b68bb0796e71878815640 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Thu, 17 Jul 2025 22:42:32 -0400 Subject: [PATCH] initial commit --- LICENSE | 24 +++++++++++ README.md | 37 +++++++++++++++++ foot-command.py | 107 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 168 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100755 foot-command.py diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1cbda58 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +Copyright (c) 2025 Daniel Fichtinger + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the author nor the names of its contributors may + be used to endorse or promote products derived from this software + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ea7ceed --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# foot scripts + +[foot]: https://codeberg.org/dnkl/foot +[`wtype`]: https://github.com/atx/wtype + +This repository contains scripts for [foot]. + +## `foot-command` + +This script implements a command palette for Foot. It allows you to +interactively pick a command defined in your `foot.ini` keybindings, and +simulate its corresponding keypress with [`wtype`]. Here's how it works: + +1. Parses keybindings from the `[key-bindings]` section of `foot.ini`. +2. Pipes the list of commands to a picker and waits for a selection. +3. Sends the mapped keypress to to the focused window using `wtype`. + +### Dependencies + +- [`wtype`]. +- A dmenu-compatible picker (e.g. `fuzzel`). + +### Usage + +```sh +foot-command.py [-c PATH_TO_CONFIG] [-p PICKER_COMMAND] +``` + +I recommend binding this to a key in your window manager. You need to invoke the +command when Foot is the focused window. + +### Options + +- `-c`, `--config`: Path to your `foot.ini` file. Defaults to + `$HOME/.config/foot/foot.ini`. +- `-p`, `--picker`: Picker to use (must support `dmenu` style input). Defaults + to `fuzzel --dmenu --placeholder=Select a command:`. diff --git a/foot-command.py b/foot-command.py new file mode 100755 index 0000000..3c2f31d --- /dev/null +++ b/foot-command.py @@ -0,0 +1,107 @@ +#!/bin/env python3 + +# Depends: wtype + +import os +import subprocess +from argparse import ArgumentParser +from configparser import ConfigParser + +ENV_CONFIG = "FOOT_CONFIG" +ENV_PICKER = "FOOT_PICKER" +DEFAULT_CONFIG = "$HOME/.config/foot.ini" + + +def parse_config(path: str): + config = ConfigParser(allow_unnamed_section=True) + config.read(path) + section = "key-bindings" + pairs: list[tuple[str, str]] = config.items(section, raw=True) + out: dict[str, str] = {} + for pair in pairs: + command = pair[0] + bindings = pair[1].split(" ") + if bindings[0] == "none": + continue + elif bindings[0][0] == "[": + out[command] = bindings[1] + else: + out[command] = bindings[0] + return out + + +def get_query_string(mapping: dict[str, str]) -> str: + return "\n".join(mapping.keys()) + + +def spawn_picker(cmd, query_string): + process = subprocess.Popen( + cmd, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE + ) + selection = process.communicate(input=query_string.encode("UTF-8")) + if process.returncode != 2: + return selection[0].decode("UTF-8").strip("\n") + else: + return None + + +MOD_MAP = { + "Control": "ctrl", + "Shift": "shift", + "Mod1": "alt", +} + + +def get_wtype_args(selection: str, mapping: dict[str, str]): + binding = mapping[selection] + args: list[str] = ["wtype"] + for k in binding.split("+"): + if k in MOD_MAP: + args += ["-M", MOD_MAP[k]] + else: + args += ["-k", k] + return args + + +def send_keys(args): + process = subprocess.run(args) + process.check_returncode() + + +def validate_path(path: str) -> str: + if path == DEFAULT_CONFIG: + return str(os.environ["HOME"]) + "/.config/foot/foot.ini" + else: + return path + + +if __name__ == "__main__": + parser = ArgumentParser() + parser.add_argument( + "-c", + "--config", + required=False, + help="Absolute path to foot.ini file. (default: %(default)s)", + default=DEFAULT_CONFIG, + type=str, + metavar=ENV_CONFIG, + ) + parser.add_argument( + "-p", + "--picker", + required=False, + help="Picker command to be used. (default: %(default)s)", + type=str, + default="fuzzel --dmenu --placeholder=Select a command:", + metavar=ENV_PICKER, + ) + args = parser.parse_args() + picker = args.picker + foot_path = validate_path(args.config) + mapping = parse_config(foot_path) + query = get_query_string(mapping) + selection = spawn_picker(picker, query) + if selection is not None: + wtype_args = get_wtype_args(selection, mapping) + print(wtype_args) + send_keys(wtype_args)