* Add Live Activities as configurable bar items * Fix activity owner monitoring and Morph list interactions * Keep activity rows stable during updates and tighten their layout * Use subtle theme tint for activity cards * Tighten activity title and progress spacing * Use theme accent for active progress rings * Fit compact activities to measured titles and badge * Expose Live Activities display controls in Settings * Open activity terminals and streamline the Morph header * Give Morph continuous icons and prioritize workspace navigation * Show activity progress inside its ring * Inset activity progress rings and strengthen their stroke * Fix activity lifecycle, command output handling and list navigation * Reduce activity update work and harden publisher lifetimes * Set up Live Activities for the detected user shell during installation * Document Activities setup, daily use and troubleshooting * Preserve Morph idle width across empty activity updates * Fix dimming flicker, hide inactive music tab and consolidate Activities docs * Fix panel clearance across styles and match previews to theme settings * Prevent panel blur from repainting neighboring islands --------- Co-authored-by: Samuel Onoja <[email protected]>
133 lines
6.0 KiB
Python
133 lines
6.0 KiB
Python
"""Hook composition/status checks use a fake publisher and never run metadata."""
|
|
from pathlib import Path
|
|
import fcntl
|
|
import json
|
|
import os
|
|
import pty
|
|
import select
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
import termios
|
|
import time
|
|
import unittest
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
|
|
class ActivityHooks(unittest.TestCase):
|
|
def test_real_interactive_commands_publish_start_and_exit_status(self):
|
|
for shell in ('bash', 'fish'):
|
|
if not shutil.which(shell):
|
|
continue
|
|
with self.subTest(shell=shell), tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
publisher = root / 'feresectl'
|
|
publisher.write_text('#!/usr/bin/env python3\nimport json, os, sys\n'
|
|
'with open(os.environ["ACTIVITY_TEST_LOG"], "a") as f:\n'
|
|
' f.write(json.dumps(sys.argv[1:]) + "\\n")\n')
|
|
publisher.chmod(0o755)
|
|
log = root / 'calls'
|
|
startup = root / 'startup'
|
|
dependency = f'source "{ROOT}/contrib/activities/bash-preexec/bash-preexec.sh"\n' if shell == 'bash' else ''
|
|
startup.write_text(dependency + f'source "{ROOT}/contrib/activities/ferese.{shell}"\n')
|
|
master, slave = pty.openpty()
|
|
env = dict(os.environ, HOME=directory, XDG_CONFIG_HOME=directory, HISTFILE='/dev/null',
|
|
PATH=str(root) + ':' + os.environ['PATH'], ACTIVITY_TEST_LOG=str(log), TERM='dumb', PS1='ACTIVITY_PROMPT> ')
|
|
command = (['bash', '--noprofile', '--rcfile', str(startup), '-i'] if shell == 'bash' else
|
|
['fish', '--no-config', '-i', '--init-command', f'function fish_prompt; printf "ACTIVITY_PROMPT> "; end; source {startup}'])
|
|
process = subprocess.Popen(command, env=env, stdin=slave, stdout=slave, stderr=slave,
|
|
start_new_session=True, preexec_fn=lambda: fcntl.ioctl(0, termios.TIOCSCTTY, 0))
|
|
os.close(slave)
|
|
trace = b''
|
|
def wait_for(predicate):
|
|
nonlocal trace
|
|
deadline = time.monotonic() + 5
|
|
while time.monotonic() < deadline:
|
|
if select.select([master], [], [], .05)[0]:
|
|
trace += os.read(master, 65536)
|
|
if predicate():
|
|
return
|
|
self.fail(trace.decode(errors='replace'))
|
|
def calls():
|
|
return [json.loads(line) for line in log.read_text().splitlines()] if log.exists() else []
|
|
try:
|
|
# Wait for the first prompt before sending an interactive command.
|
|
wait_for(lambda: b'# ' in trace or b'$ ' in trace or b'> ' in trace)
|
|
os.write(master, b'false\n')
|
|
wait_for(lambda: any(call[:2] == ['activity', 'end'] for call in calls()))
|
|
records = calls()
|
|
start = next(call for call in records if call[:2] == ['activity', 'hook'])
|
|
end = next(call for call in records if call[:2] == ['activity', 'end'])
|
|
self.assertEqual(start[start.index('--command') + 1], 'false')
|
|
self.assertEqual(end[2], start[2])
|
|
self.assertEqual(end[end.index('--result') + 1], 'fail')
|
|
finally:
|
|
process.terminate()
|
|
try:
|
|
process.wait(timeout=2)
|
|
except subprocess.TimeoutExpired:
|
|
process.kill()
|
|
process.wait()
|
|
os.close(master)
|
|
|
|
@unittest.skipUnless(shutil.which("fish"), "fish unavailable")
|
|
def test_fish_preserves_status_and_does_not_execute_metadata(self):
|
|
script = f'''function feresectl; return 71; end
|
|
source "{ROOT}/contrib/activities/ferese.fish"
|
|
__ferese_preexec 'echo SHOULD_NOT_EXECUTE'
|
|
false
|
|
__ferese_postexec
|
|
set saved $status
|
|
printf 'status=%s\\n' $saved
|
|
'''
|
|
result = subprocess.run(["fish", "--no-config", "-i", "-c", script], capture_output=True, text=True, timeout=10)
|
|
self.assertIn("status=1", result.stdout)
|
|
self.assertNotIn("SHOULD_NOT_EXECUTE", result.stdout)
|
|
|
|
def test_bash_preserves_status_and_never_executes_captured_command(self):
|
|
script = f'''
|
|
feresectl() {{ return 71; }}
|
|
source "{ROOT}/contrib/activities/bash-preexec/bash-preexec.sh"
|
|
source "{ROOT}/contrib/activities/ferese.bash"
|
|
_ferese_arm
|
|
_ferese_preexec 'echo SHOULD_NOT_EXECUTE'
|
|
false
|
|
_ferese_finish
|
|
saved=$?
|
|
printf 'status=%s\\n' "$saved"
|
|
'''
|
|
result = subprocess.run(["bash", "--norc", "-i", "-c", script], capture_output=True, text=True, timeout=10)
|
|
self.assertIn("status=1", result.stdout)
|
|
self.assertNotIn("SHOULD_NOT_EXECUTE", result.stdout)
|
|
|
|
def test_bash_preserves_existing_debug_trap(self):
|
|
script = f'''trap ':' DEBUG
|
|
before=$(trap -p DEBUG)
|
|
source "{ROOT}/contrib/activities/ferese.bash"
|
|
after=$(trap -p DEBUG)
|
|
[[ "$before" == "$after" ]]
|
|
'''
|
|
result = subprocess.run(["bash", "--norc", "-i", "-c", script], capture_output=True, text=True, timeout=10)
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
|
|
@unittest.skipUnless(shutil.which("zsh"), "zsh unavailable")
|
|
def test_zsh_composes_hooks_and_preserves_status(self):
|
|
script = f'''feresectl() {{ return 71; }}
|
|
existing() {{ :; }}
|
|
autoload -Uz add-zsh-hook
|
|
add-zsh-hook precmd existing
|
|
source "{ROOT}/contrib/activities/ferese.zsh"
|
|
(( ${{precmd_functions[(Ie)existing]}} > 0 )) || exit 4
|
|
_ferese_preexec 'echo SHOULD_NOT_EXECUTE'
|
|
false
|
|
_ferese_precmd
|
|
saved=$?
|
|
printf 'status=%s\\n' "$saved"
|
|
'''
|
|
result = subprocess.run(["zsh", "-f", "-i", "-c", script], capture_output=True, text=True, timeout=10)
|
|
self.assertIn("status=1", result.stdout)
|
|
self.assertNotIn("SHOULD_NOT_EXECUTE", result.stdout)
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|