Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions pysus/native_dir_picker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""Cross-platform native directory picker."""

import os
import platform
import subprocess

_WINDOWS_SCRIPT = """
Add-Type -AssemblyName System.Windows.Forms
$f = New-Object System.Windows.Forms.FolderBrowserDialog
$f.Description = $env:PYSUS_DIALOG_TITLE
$f.SelectedPath = $env:PYSUS_DIALOG_INITIALDIR
$f.ShowDialog() | Out-Null
$f.SelectedPath
"""

_MACOS_SCRIPT = """
set dialogTitle to system attribute "PYSUS_DIALOG_TITLE"
set initialDirectory to system attribute "PYSUS_DIALOG_INITIALDIR"
tell application "System Events"
activate
set f to choose folder with prompt dialogTitle ¬
default location POSIX file initialDirectory
POSIX path of f
end tell
"""


def _dialog_environment(title: str, initialdir: str) -> dict[str, str]:
env = os.environ.copy()
env["PYSUS_DIALOG_TITLE"] = title
env["PYSUS_DIALOG_INITIALDIR"] = initialdir
return env


def native_dir_picker(title: str, initialdir: str) -> str:
"""Open a native directory picker and return the selected path.

Values are passed as command arguments or environment variables instead
of being interpolated into scripts executed by platform interpreters.
"""
system = platform.system()

if system == "Linux":
for cmd in (
[
"zenity",
"--file-selection",
"--directory",
f"--filename={initialdir}/",
f"--title={title}",
],
["kdialog", "--getexistingdirectory", initialdir, "--title", title],
):
try:
result = subprocess.run(
cmd, capture_output=True, text=True, timeout=30
)
return result.stdout.strip()
except (FileNotFoundError, subprocess.TimeoutExpired):
continue

elif system == "Windows":
result = subprocess.run(
[
"powershell",
"-NoProfile",
"-NonInteractive",
"-Command",
_WINDOWS_SCRIPT,
],
capture_output=True,
text=True,
env=_dialog_environment(title, initialdir),
)
return result.stdout.strip()

elif system == "Darwin":
result = subprocess.run(
["osascript", "-e", _MACOS_SCRIPT],
capture_output=True,
text=True,
env=_dialog_environment(title, initialdir),
)
return result.stdout.strip()

return ""
96 changes: 96 additions & 0 deletions pysus/tests/web/test_native_dir_picker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import subprocess
from unittest.mock import Mock, patch

from pysus.native_dir_picker import native_dir_picker

_TITLE = "Selecionar exportacao da unidade 'APS Central'"
_INITIALDIR = 'C:\\Dados APS\\Unidade "Central"'


@patch("pysus.native_dir_picker.subprocess.run")
@patch("pysus.native_dir_picker.platform.system", return_value="Windows")
def test_windows_values_are_passed_through_environment(_, run):
run.return_value = Mock(stdout="C:\\selected\n")

assert native_dir_picker(_TITLE, _INITIALDIR) == "C:\\selected"

args = run.call_args.args[0]
kwargs = run.call_args.kwargs
assert _TITLE not in args[-1]
assert _INITIALDIR not in args[-1]
assert kwargs["env"]["PYSUS_DIALOG_TITLE"] == _TITLE
assert kwargs["env"]["PYSUS_DIALOG_INITIALDIR"] == _INITIALDIR


@patch("pysus.native_dir_picker.subprocess.run")
@patch("pysus.native_dir_picker.platform.system", return_value="Darwin")
def test_macos_values_are_passed_through_environment(_, run):
run.return_value = Mock(stdout="/tmp/selected\n")

assert native_dir_picker(_TITLE, _INITIALDIR) == "/tmp/selected"

args = run.call_args.args[0]
kwargs = run.call_args.kwargs
assert _TITLE not in args[-1]
assert _INITIALDIR not in args[-1]
assert kwargs["env"]["PYSUS_DIALOG_TITLE"] == _TITLE
assert kwargs["env"]["PYSUS_DIALOG_INITIALDIR"] == _INITIALDIR


@patch("pysus.native_dir_picker.subprocess.run")
@patch("pysus.native_dir_picker.platform.system", return_value="Linux")
def test_linux_values_are_passed_as_arguments(_, run):
run.return_value = Mock(stdout="/tmp/selected\n")

assert native_dir_picker(_TITLE, _INITIALDIR) == "/tmp/selected"

args = run.call_args.args[0]
assert args[-1] == f"--title={_TITLE}"
assert args[-2] == f"--filename={_INITIALDIR}/"


@patch("pysus.native_dir_picker.subprocess.run")
@patch("pysus.native_dir_picker.platform.system", return_value="Linux")
def test_linux_falls_back_to_kdialog_when_zenity_is_unavailable(_, run):
run.side_effect = [
FileNotFoundError,
Mock(stdout="/tmp/selected-by-kdialog\n"),
]

assert native_dir_picker(_TITLE, _INITIALDIR) == "/tmp/selected-by-kdialog"

assert run.call_count == 2
assert run.call_args_list[1].args[0] == [
"kdialog",
"--getexistingdirectory",
_INITIALDIR,
"--title",
_TITLE,
]


@patch("pysus.native_dir_picker.subprocess.run")
@patch("pysus.native_dir_picker.platform.system", return_value="Linux")
def test_linux_returns_empty_when_all_pickers_fail(_, run):
run.side_effect = [
FileNotFoundError,
subprocess.TimeoutExpired("kdialog", 30),
]

assert native_dir_picker(_TITLE, _INITIALDIR) == ""
assert run.call_count == 2


@patch("pysus.native_dir_picker.subprocess.run")
@patch("pysus.native_dir_picker.platform.system", return_value="Windows")
def test_cancelled_picker_returns_empty(_, run):
run.return_value = Mock(stdout="")

assert native_dir_picker(_TITLE, _INITIALDIR) == ""


@patch("pysus.native_dir_picker.subprocess.run")
@patch("pysus.native_dir_picker.platform.system", return_value="FreeBSD")
def test_unsupported_platform_returns_empty_without_running_command(_, run):
assert native_dir_picker(_TITLE, _INITIALDIR) == ""
run.assert_not_called()
67 changes: 2 additions & 65 deletions pysus/web/pages/1_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from pysus import CACHEPATH
from pysus.api.client import PySUS
from pysus.api.models import BaseRemoteFile
from pysus.native_dir_picker import native_dir_picker
from pysus.web.translations import t

STATES = [
Expand Down Expand Up @@ -699,70 +700,6 @@ def _size_column_config() -> dict[str, Any]:
}


def _native_dir_picker(title: str, initialdir: str) -> str:
"""Open a native directory picker dialog and return the selected path."""
import platform
import subprocess

system = platform.system()

if system == "Linux":
for cmd in (
[
"zenity",
"--file-selection",
"--directory",
f"--filename={initialdir}/",
f"--title={title}",
],
["kdialog", "--getexistingdirectory", initialdir, "--title", title],
):
try:
r = subprocess.run(
cmd, capture_output=True, text=True, timeout=30
)
return r.stdout.strip()
except (FileNotFoundError, subprocess.TimeoutExpired):
continue

elif system == "Windows":
ps = f"""
Add-Type -AssemblyName System.Windows.Forms
$f = New-Object System.Windows.Forms.FolderBrowserDialog
$f.Description = '{title}'
$f.SelectedPath = '{initialdir}'
$f.ShowDialog() | Out-Null
$f.SelectedPath
"""
r = subprocess.run(
["powershell", "-Command", ps],
capture_output=True,
text=True,
)
return r.stdout.strip()

elif system == "Darwin":
prompt_line = (
'set f to choose folder with prompt "{}"'
' default location POSIX file "{}"'
).format(title, initialdir)
applescript = (
f'tell application "System Events"\n'
f" activate\n"
f" {prompt_line}\n"
f" POSIX path of f\n"
f"end tell"
)
r = subprocess.run(
["osascript", "-e", applescript],
capture_output=True,
text=True,
)
return r.stdout.strip()

return ""


def _show_results(pysus: PySUS, client: str) -> None:
query_key = f"_query_results_{client}"
queue_key = f"_download_queue_{client}"
Expand Down Expand Up @@ -879,7 +816,7 @@ def _show_results(pysus: PySUS, client: str) -> None:
)
with col_btn:
if st.button(t("browse", _lang()), width="stretch"):
folder = _native_dir_picker(
folder = native_dir_picker(
title=t("browse_dir_title", _lang()),
initialdir=st.session_state[dir_key],
)
Expand Down
Loading