@@ -1027,27 +1027,11 @@ def get_system_commands(self, screen: Screen) -> Iterable[SystemCommand]:
10271027 "Maximize" , "Maximize the focused widget" , screen .action_maximize
10281028 )
10291029
1030- # Don't save screenshot for web drivers until we have the deliver_file in place
1031- if self ._driver .__class__ .__name__ in {"LinuxDriver" , "WindowsDriver" }:
1032-
1033- def export_screenshot () -> None :
1034- """Export a screenshot and write a notification."""
1035- filename = self .save_screenshot ()
1036- try :
1037- self .notify (f"Saved { filename } " , title = "Screenshot" )
1038- except Exception as error :
1039- self .log .error (error )
1040- self .notify (
1041- "Failed to save screenshot." ,
1042- title = "Screenshot" ,
1043- severity = "warning" ,
1044- )
1045-
1046- yield SystemCommand (
1047- "Save screenshot" ,
1048- "Save an SVG 'screenshot' of the current screen (in the current working directory)" ,
1049- export_screenshot ,
1050- )
1030+ yield SystemCommand (
1031+ "Save screenshot" ,
1032+ "Save an SVG 'screenshot' of the current screen" ,
1033+ self .deliver_screenshot ,
1034+ )
10511035
10521036 def get_default_screen (self ) -> Screen :
10531037 """Get the default screen.
@@ -1385,14 +1369,16 @@ def action_toggle_dark(self) -> None:
13851369 """An [action](/guide/actions) to toggle dark mode."""
13861370 self .dark = not self .dark
13871371
1388- def action_screenshot (self , filename : str | None = None , path : str = "./" ) -> None :
1372+ def action_screenshot (
1373+ self , filename : str | None = None , path : str | None = None
1374+ ) -> None :
13891375 """This [action](/guide/actions) will save an SVG file containing the current contents of the screen.
13901376
13911377 Args:
13921378 filename: Filename of screenshot, or None to auto-generate.
1393- path: Path to directory. Defaults to current working directory.
1379+ path: Path to directory. Defaults to the user's Downloads directory.
13941380 """
1395- self .save_screenshot (filename , path )
1381+ self .deliver_screenshot (filename , path )
13961382
13971383 def export_screenshot (self , * , title : str | None = None ) -> str :
13981384 """Export an SVG screenshot of the current screen.
@@ -1451,6 +1437,42 @@ def save_screenshot(
14511437 svg_file .write (screenshot_svg )
14521438 return svg_path
14531439
1440+ def deliver_screenshot (
1441+ self ,
1442+ filename : str | None = None ,
1443+ path : str | None = None ,
1444+ time_format : str | None = None ,
1445+ ) -> str | None :
1446+ """Deliver a screenshot of the app.
1447+
1448+ This with save the screenshot when running locally, or serve it when the app
1449+ is running in a web browser.
1450+
1451+ Args:
1452+ filename: Filename of SVG screenshot, or None to auto-generate
1453+ a filename with the date and time.
1454+ path: Path to directory for output when saving locally (not used when app is running in the browser).
1455+ Defaults to current working directory.
1456+ time_format: Date and time format to use if filename is None.
1457+ Defaults to a format like ISO 8601 with some reserved characters replaced with underscores.
1458+
1459+ Returns:
1460+ The delivery key that uniquely identifies the file delivery.
1461+ """
1462+ if not filename :
1463+ svg_filename = generate_datetime_filename (self .title , ".svg" , time_format )
1464+ else :
1465+ svg_filename = filename
1466+ screenshot_svg = self .export_screenshot ()
1467+ return self .deliver_text (
1468+ io .StringIO (screenshot_svg ),
1469+ save_directory = path ,
1470+ save_filename = svg_filename ,
1471+ open_method = "browser" ,
1472+ mime_type = "image/svg+xml" ,
1473+ name = "screenshot" ,
1474+ )
1475+
14541476 def bind (
14551477 self ,
14561478 keys : str ,
@@ -3926,6 +3948,7 @@ def deliver_text(
39263948 open_method : Literal ["browser" , "download" ] = "download" ,
39273949 encoding : str | None = None ,
39283950 mime_type : str | None = None ,
3951+ name : str | None = None ,
39293952 ) -> str | None :
39303953 """Deliver a text file to the end-user of the application.
39313954
@@ -3956,6 +3979,8 @@ def deliver_text(
39563979 mime_type: The MIME type of the file or None to guess based on file extension.
39573980 If no MIME type is supplied and we cannot guess the MIME type, from the
39583981 file extension, the MIME type will be set to "text/plain".
3982+ name: A user-defined named which will be returned in [`DeliveryComplete`][textual.events.DeliveryComplete]
3983+ and [`DeliveryComplete`][textual.events.DeliveryComplete].
39593984
39603985 Returns:
39613986 The delivery key that uniquely identifies the file delivery.
@@ -3985,6 +4010,7 @@ def deliver_text(
39854010 open_method = open_method ,
39864011 encoding = encoding ,
39874012 mime_type = mime_type ,
4013+ name = name ,
39884014 )
39894015
39904016 def deliver_binary (
@@ -3995,6 +4021,7 @@ def deliver_binary(
39954021 save_filename : str | None = None ,
39964022 open_method : Literal ["browser" , "download" ] = "download" ,
39974023 mime_type : str | None = None ,
4024+ name : str | None = None ,
39984025 ) -> str | None :
39994026 """Deliver a binary file to the end-user of the application.
40004027
@@ -4033,6 +4060,8 @@ def deliver_binary(
40334060 mime_type: The MIME type of the file or None to guess based on file extension.
40344061 If no MIME type is supplied and we cannot guess the MIME type, from the
40354062 file extension, the MIME type will be set to "application/octet-stream".
4063+ name: A user-defined named which will be returned in [`DeliveryComplete`][textual.events.DeliveryComplete]
4064+ and [`DeliveryComplete`][textual.events.DeliveryComplete].
40364065
40374066 Returns:
40384067 The delivery key that uniquely identifies the file delivery.
@@ -4061,6 +4090,7 @@ def deliver_binary(
40614090 open_method = open_method ,
40624091 mime_type = mime_type ,
40634092 encoding = None ,
4093+ name = name ,
40644094 )
40654095
40664096 def _deliver_binary (
@@ -4072,10 +4102,11 @@ def _deliver_binary(
40724102 open_method : Literal ["browser" , "download" ],
40734103 encoding : str | None = None ,
40744104 mime_type : str | None = None ,
4105+ name : str | None = None ,
40754106 ) -> str | None :
40764107 """Deliver a binary file to the end-user of the application."""
40774108 if self ._driver is None :
4078- return
4109+ return None
40794110
40804111 # Generate a filename if the file-like object doesn't have one.
40814112 if save_filename is None :
@@ -4099,6 +4130,27 @@ def _deliver_binary(
40994130 encoding = encoding ,
41004131 open_method = open_method ,
41014132 mime_type = mime_type ,
4133+ name = name ,
41024134 )
41034135
41044136 return delivery_key
4137+
4138+ @on (events .DeliveryComplete )
4139+ def _on_delivery_complete (self , event : events .DeliveryComplete ) -> None :
4140+ """Handle a successfully delivered screenshot."""
4141+ if event .name == "screenshot" :
4142+ if event .path is None :
4143+ self .notify ("Saved screenshot" , title = "Screenshot" )
4144+ else :
4145+ self .notify (
4146+ f"Saved screenshot to [green]{ str (event .path )!r} " ,
4147+ title = "Screenshot" ,
4148+ )
4149+
4150+ @on (events .DeliveryFailed )
4151+ def _on_delivery_failed (self , event : events .DeliveryComplete ) -> None :
4152+ """Handle a failure to deliver the screenshot."""
4153+ if event .name == "screenshot" :
4154+ self .notify (
4155+ "Failed to save screenshot" , title = "Screenshot" , severity = "error"
4156+ )
0 commit comments