mirror of
https://github.com/yusufipk/dikte.git
synced 2026-09-11 19:06:11 +00:00
The macOS backends were already here: CoreAudio capture through ffmpeg, pbcopy and CoreGraphics, Carbon hotkeys, the paths under ~/Library. What was missing was everything that installs them, so install.sh hands over to install-mac.sh on Darwin rather than growing a branch per line: the XDG directories, the .desktop files and the shortcut registry mean nothing there, and an application is a bundle rather than a path. The bundle carries a copy of the interpreter, because macOS files the microphone and Accessibility permissions against the process that asks, and a launcher running Homebrew's python3 would have asked as python3 and shared the grant with everything else on that interpreter. It is signed ad-hoc so a reinstall is the same application rather than two more dialogs, and it says so plainly when a brew upgrade has moved the tree it needs. uninstall.sh and update.sh follow it. The tray icon was invisible: QIcon.fromTheme wants a freedesktop icon theme and hands back a null icon without one, which in a menu bar is the whole interface gone. trayicon.py draws the three shapes as template images, so they follow the menu bar into dark mode, and the bundle's icon comes off the same glyph rather than a binary in the repository. Linux keeps its own icons; these are used only where the theme has nothing. paths.py is the fix that was never about a Mac. config.py imports ggml.py, so ggml.py could not ask it where the data goes; each worked it out for itself and only one of them knew about macOS. Settings went to ~/Library while several gigabytes of models went to ~/.local/share, which is not a place a Mac user looks and not a place uninstall.sh --purge would have deleted from. Ctrl+Space is the input-source switch there and Cmd+Space is Spotlight, so the default is Ctrl+Option+Space, and hotkey.default_combo is the one place that difference lives. The first paste asks for Accessibility with kAXTrustedCheckOptionPrompt, which is what creates the row to switch on; asking the other way opens a pane Dikte is not listed in. `dikte shortcut status` asks the running instance, since the combination is held by that process and by nothing else. Local speech to text is the one piece a Mac builds by hand. whisper.cpp publishes no macOS binary and Homebrew's is configured with WHISPER_BUILD_SERVER=OFF, so it installs whisper-cli and not the server Dikte talks to; program_path already takes a whisper-server off the PATH or out of Settings, so the answer is the one a Linux distribution gets, and the README carries the cmake line. CI grows a macOS job on 3.11 and 3.13, the only place the Carbon and CoreGraphics libraries have to be there to be opened. Written and tested on macOS 27.0 arm64. Two things are still unverified on a Mac: the paste end to end, which waits on the Accessibility toggle, and a meeting recording, which needs a loopback driver.
160 lines
6.0 KiB
Bash
Executable File
160 lines
6.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Dikte installer: dependency check, launchers, global shortcuts.
|
|
set -euo pipefail
|
|
|
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
# Nothing below this line is true on a Mac: no XDG directories, no .desktop
|
|
# files, no shortcut registry, and an application is a bundle rather than a
|
|
# path. That is a second script rather than a branch through this one, and
|
|
# update.sh reaches it through here without having to know which it is on.
|
|
if [[ "$(uname -s)" == "Darwin" ]]; then
|
|
exec "$DIR/install-mac.sh" "$@"
|
|
fi
|
|
|
|
PY="$(command -v python3)"
|
|
BIN_DIR="$HOME/.local/bin"
|
|
APP_DIR="$HOME/.local/share/applications"
|
|
AUTOSTART_DIR="$HOME/.config/autostart"
|
|
SHORTCUT="${1:-Ctrl+Space}"
|
|
# Without the colon, so that a second argument given as "" stays empty. That is
|
|
# how update.sh says "this one was turned off", as against not saying anything.
|
|
CANCEL_SHORTCUT="${2-Ctrl+Alt+Space}"
|
|
|
|
say() { printf ' %s\n' "$1"; }
|
|
ok() { printf ' \033[32m✓\033[0m %s\n' "$1"; }
|
|
warn() { printf ' \033[33m!\033[0m %s\n' "$1"; }
|
|
|
|
echo
|
|
echo "Installing Dikte"
|
|
echo "────────────────"
|
|
|
|
# 1. Dependencies ----------------------------------------------------------
|
|
missing=()
|
|
audio_cmds=(ffmpeg)
|
|
if command -v parec >/dev/null || command -v pw-record >/dev/null; then
|
|
:
|
|
else
|
|
missing+=("pulseaudio-utils-or-pipewire-audio")
|
|
fi
|
|
if [[ "${XDG_SESSION_TYPE:-}" == "x11" ]]; then
|
|
desktop_cmds=(xclip xdotool)
|
|
else
|
|
desktop_cmds=(wl-copy wl-paste ydotool)
|
|
fi
|
|
for cmd in "${audio_cmds[@]}" "${desktop_cmds[@]}"; do
|
|
command -v "$cmd" >/dev/null || missing+=("$cmd")
|
|
done
|
|
python3 -c 'import PyQt6.QtWidgets' 2>/dev/null || missing+=("python-pyqt6")
|
|
|
|
if ((${#missing[@]})); then
|
|
warn "Missing: ${missing[*]}"
|
|
say "Ubuntu X11: sudo apt install pulseaudio-utils xclip xdotool ffmpeg"
|
|
say "Arch Wayland: sudo pacman -S --needed pipewire-audio wl-clipboard ydotool ffmpeg python-pyqt6"
|
|
say "Fedora Wayland: sudo dnf install pipewire-utils wl-clipboard ydotool ffmpeg-free python3-pyqt6"
|
|
echo
|
|
else
|
|
ok "All dependencies present"
|
|
fi
|
|
|
|
# 2. ydotoold --------------------------------------------------------------
|
|
# What auto-paste needs is a socket it may write to, which is not the same
|
|
# question as whether the unit is up: Fedora ships ydotool as a system service
|
|
# only, and its socket stays root-owned at mode 600, so there the daemon can be
|
|
# running while every paste is refused. The socket file outlives the daemon,
|
|
# though, so the process has to be there as well for the answer to be yes.
|
|
if [[ "${XDG_SESSION_TYPE:-}" != "x11" ]] && command -v ydotool >/dev/null; then
|
|
socket="${YDOTOOL_SOCKET:-${XDG_RUNTIME_DIR:-/tmp}/.ydotool_socket}"
|
|
alive() { pgrep -x ydotoold >/dev/null 2>&1; }
|
|
if [[ -w "$socket" ]] && alive; then
|
|
ok "ydotoold is running (auto-paste ready)"
|
|
elif systemctl is-active --quiet ydotool 2>/dev/null; then
|
|
warn "ydotoold's socket is not yours to write to, so auto-paste will fail"
|
|
say "Hand it over with the drop-in in the README's Fedora section."
|
|
elif alive; then
|
|
warn "ydotoold is running, but it did not put its socket at $socket"
|
|
say "Point Dikte at the one it did make: export YDOTOOL_SOCKET=..."
|
|
else
|
|
warn "ydotoold is not running, auto-paste will not work"
|
|
say "systemctl --user enable --now ydotool (on Fedora: see the README)"
|
|
fi
|
|
fi
|
|
|
|
# 3. Launchers -------------------------------------------------------------
|
|
mkdir -p "$BIN_DIR" "$APP_DIR" "$AUTOSTART_DIR"
|
|
ln -sf "$DIR/dikte.py" "$BIN_DIR/dikte"
|
|
chmod +x "$DIR/dikte.py"
|
|
ok "Command installed: $BIN_DIR/dikte"
|
|
case ":$PATH:" in
|
|
*":$BIN_DIR:"*) ;;
|
|
*) warn "$BIN_DIR is not on your PATH. For fish: fish_add_path $BIN_DIR" ;;
|
|
esac
|
|
|
|
cat > "$APP_DIR/dikte.desktop" <<EOF
|
|
[Desktop Entry]
|
|
Type=Application
|
|
Name=Dikte
|
|
Comment=Voice dictation: record, transcribe, clean up, paste
|
|
Exec=$PY $DIR/dikte.py
|
|
Icon=audio-input-microphone
|
|
Categories=Utility;AudioVideo;
|
|
StartupNotify=false
|
|
EOF
|
|
ok "Application menu entry added"
|
|
|
|
cat > "$AUTOSTART_DIR/dikte.desktop" <<EOF
|
|
[Desktop Entry]
|
|
Type=Application
|
|
Name=Dikte
|
|
Exec=$PY $DIR/dikte.py
|
|
Icon=audio-input-microphone
|
|
X-GNOME-Autostart-enabled=true
|
|
StartupNotify=false
|
|
EOF
|
|
ok "Will start automatically on login"
|
|
|
|
# 4. Global shortcuts ------------------------------------------------------
|
|
# Two of them: one to start and stop, one to throw the recording away. The
|
|
# second is worth a key of its own because stopping is the step there is no
|
|
# taking back, being what sends the audio off to be transcribed.
|
|
#
|
|
# Dikte registers them rather than this script writing the files itself: it
|
|
# knows which desktop it is on, and it stores the combination in the settings
|
|
# as well, which is where the built-in listener reads it from. A key written
|
|
# to only one of the two places is a key that half works.
|
|
if [[ "$SHORTCUT" == "$CANCEL_SHORTCUT" ]]; then
|
|
warn "Both arguments are $SHORTCUT, so the discard key was left out."
|
|
say "Pass two different combinations, or set it in Settings → Shortcuts."
|
|
CANCEL_SHORTCUT=""
|
|
fi
|
|
|
|
register() { # which combination label
|
|
if out="$("$PY" "$DIR/dikte.py" shortcut install "$1" --combo "$2" 2>&1)"; then
|
|
ok "$3: $2"
|
|
else
|
|
# One line: the rest of what it has to say about KWin is printed below.
|
|
warn "${out%%$'\n'*}"
|
|
fi
|
|
}
|
|
|
|
if python3 -c 'import PyQt6.QtWidgets' 2>/dev/null; then
|
|
register toggle "$SHORTCUT" "Start and stop"
|
|
if [[ -n "$CANCEL_SHORTCUT" ]]; then
|
|
register cancel "$CANCEL_SHORTCUT" "Discard the recording"
|
|
fi
|
|
if [[ "${XDG_CURRENT_DESKTOP:-}" != *[Gg][Nn][Oo][Mm][Ee]* ]]; then
|
|
warn "KWin only reads these at startup, so they go live after your next"
|
|
say "login. Until then open Settings → Shortcuts and turn on the"
|
|
say "built-in listener to use them right away."
|
|
fi
|
|
else
|
|
warn "PyQt6 is missing, so no shortcut was registered. Install it, then run:"
|
|
say "dikte shortcut install toggle --combo '$SHORTCUT'"
|
|
fi
|
|
|
|
echo
|
|
ok "Done. Start it with: dikte"
|
|
say "The settings window opens on first run: download a speech model, or add"
|
|
say "an OpenAI or OpenRouter key instead."
|
|
echo
|