Add Live Activities as configurable bar items (#30)
* 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]>
This commit is contained in:
1 parent
7a7df0106c
commit
6b0d903ad0
107 files changed
+9340
-291
No files matched your search
@@ -0,0 +1,132 @@
|
||||
"""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()
|
||||
Reference in new issue
Block a user