feat: first version
check / tests and shellcheck (push) Failing after 56s

This commit is contained in:
Felitendo committed 2026-10-02 10:39:46 +02:00
commit 3e98dd1e7e
48 files changed
+13025

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto
+1
View File
@@ -0,0 +1 @@
ko_fi: felitendo
@@ -0,0 +1,35 @@
name: 💡 Feature request
description: An idea to make rdfeed better.
labels:
- enhancement
body:
- type: checkboxes
id: checks
attributes:
label: Before you start
options:
- label: I searched the issues and this is not requested yet.
required: true
- type: textarea
id: problem
attributes:
label: What problem would this solve?
description: What are you trying to do, and what gets in the way?
validations:
required: true
- type: textarea
id: idea
attributes:
label: What would you like?
description: How it could work. A rough idea is fine.
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Other ways you thought of
- type: textarea
id: more
attributes:
label: Anything else?
description: Mockups, screenshots, or other tools that do it well.
+98
View File
@@ -0,0 +1,98 @@
name: 🐛 Bug report
description: Something does not work as it should.
labels:
- bug
body:
- type: markdown
attributes:
value: Thanks for taking the time! The more you fill in, the faster it can be fixed. Please leave out your company's server names and your user name.
- type: checkboxes
id: checks
attributes:
label: Before you start
options:
- label: I searched the issues and this is not reported yet.
required: true
- label: I use the latest version.
required: true
- type: textarea
id: what
attributes:
label: What happened?
description: What did you do, and what went wrong?
validations:
required: true
- type: textarea
id: steps
attributes:
label: Steps to reproduce
placeholder: |
1.
2.
3.
- type: textarea
id: expected
attributes:
label: What did you expect?
validations:
required: true
- type: dropdown
id: where
attributes:
label: Where does it happen?
multiple: true
options:
- Adding a workspace
- Starting an app
- The app window (drawing, keyboard, size)
- MFA prompts
- The app menu
- The menu
- Other
validations:
required: true
- type: dropdown
id: mfa
attributes:
label: Does your company use MFA?
options:
- "Yes"
- "No"
- I don't know
- type: input
id: version
attributes:
label: Version
description: The output of `rdfeed --version` and `xfreerdp3 /version`.
placeholder: rdfeed 1.0.0, FreeRDP 3.32.1
validations:
required: true
- type: dropdown
id: install
attributes:
label: How did you install it?
options:
- AUR (Arch, CachyOS, EndeavourOS, Manjaro)
- Built from source
- Other
validations:
required: true
- type: input
id: system
attributes:
label: System
description: Distribution, desktop and its version, Wayland or X11.
placeholder: CachyOS, KDE Plasma 6.7, Wayland
validations:
required: true
- type: textarea
id: logs
attributes:
label: Log
description: For a start that goes wrong, the log from `~/.cache/rdfeed/` (one file per app). Remove server names and user names first.
render: shell
- type: textarea
id: more
attributes:
label: Anything else?
description: Screenshots or anything else that could help.
+1
View File
@@ -0,0 +1 @@
blank_issues_enabled: false
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
#
# Refuses a release whose tag and Makefile disagree.
#
# The version is baked into the program at install time from the Makefile, and
# the packages take theirs from the same place. But the tag is what people see
# and what the release is named after. A tag that says something else produces
# a package called 1.0.4 containing a program that reports 1.0.3, and nothing
# would have complained.
#
# Anything that is not a v-tag (a run started by hand from a branch) is not a
# release and has nothing to check.
set -euo pipefail
here="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
ref="${1:-}"
case "$ref" in
v[0-9]*) ;;
*)
echo "not a release tag (${ref:-none}), nothing to check against"
exit 0
;;
esac
tag_version="${ref#v}"
make_version="$(make -s -C "$here" version)"
if [[ $tag_version != "$make_version" ]]; then
echo "tag $ref says $tag_version, the Makefile says $make_version" >&2
echo "Bump VERSION in the Makefile to match the tag, or retag." >&2
exit 1
fi
echo "$ref matches the Makefile"
+136
View File
@@ -0,0 +1,136 @@
#!/usr/bin/env bash
#
# Builds the GitHub release notes for a tag, the way big projects such as
# Immich lay them out: the hand-written entry from CHANGELOG.md (a welcome,
# the highlights), a support section, then every commit since the last
# release, sorted by kind, and a link to the full changelog.
#
# .github/release-notes.sh v1.2.0 the notes
# .github/release-notes.sh --title v1.2.0 the title (checks the entry exists)
#
# Commit authors come from the GitHub API through gh. Without it the list
# goes without them.
set -euo pipefail
cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.."
title=0
if [[ ${1:-} == --title ]]; then
title=1
shift
fi
tag="${1:?usage: release-notes.sh [--title] vX.Y.Z}"
if ! grep -qx "## $tag" CHANGELOG.md; then
echo "CHANGELOG.md has no entry for $tag. Add \"## $tag\" first." >&2
exit 1
fi
if (( title )); then
printf '%s\n' "$tag"
exit 0
fi
repo="${GITHUB_REPOSITORY:-$(git remote get-url origin | sed -E 's#^.*github\.com[:/]##; s#\.git$##')}"
name="${repo#*/}"
# The entry: up to the next one, without the date line (GitHub shows the
# date), one heading level up, outside code blocks.
entry="$(awk -v h="## $tag" '
$0 == h { on = 1; next }
on && /^## / { exit }
!on { next }
/^```/ { code = !code }
!code && /^_[0-9]{4}-[0-9]{2}-[0-9]{2}_$/ { next }
!code && /^###/ { sub(/^#/, "") }
{ print }
' CHANGELOG.md | sed -e '/./,$!d')"
# The commits: from the last release, or from the start. A tag that does not
# exist yet is a preview of what HEAD would become.
if git rev-parse -q --verify "refs/tags/$tag" > /dev/null; then
target="$tag"
prev="$(git describe --tags --abbrev=0 --match 'v[0-9]*' "$tag^" 2>/dev/null || true)"
else
target="$(git rev-parse HEAD)"
prev="$(git describe --tags --abbrev=0 --match 'v[0-9]*' HEAD 2>/dev/null || true)"
fi
declare -A login=()
if command -v gh > /dev/null; then
if [[ -n $prev ]]; then
api=(api "repos/$repo/compare/$prev...$target" --jq '.commits[] | [.sha, (.author.login // "")] | @tsv')
else
api=(api --paginate "repos/$repo/commits?sha=$target&per_page=100" --jq '.[] | [.sha, (.author.login // "")] | @tsv')
fi
while IFS=$'\t' read -r sha who; do
[[ -n $who ]] && login[$sha]="$who"
done < <(gh "${api[@]}" 2>/dev/null || true)
fi
declare -A list=()
count_maint=0
while IFS=$'\t' read -r sha subject body; do
# Version bumps say nothing a reader needs.
[[ $subject =~ ^(chore:\ )?($name\ )?v?[0-9]+\.[0-9]+\.[0-9]+$ ]] && continue
case "$subject" in
*!:*) kind=breaking ;;
feat:* | feat\(*) kind=feat ;;
fix:* | fix\(*) kind=fix ;;
perf:* | perf\(*) kind=enh ;;
docs:* | docs\(*) kind=docs ;;
chore* | ci:* | ci\(* | build* | refactor* | test* | style*) kind=maint ;;
# Older commits without a prefix, sorted by what they say.
*README* | *readme* | *Readme*) kind=docs ;;
Add\ * | Put\ * | Introduce\ *) kind=feat ;;
Fix\ * | Repair\ * | Recover\ * | Stop\ *) kind=fix ;;
*\ *) kind=enh ;;
*) kind=maint ;;
esac
[[ $body == *"BREAKING CHANGE"* ]] && kind=breaking
[[ $subject == "Initial commit" ]] && kind=maint
line="* $subject"
[[ -n ${login[$sha]:-} ]] && line+=" by @${login[$sha]}"
line+=" in https://github.com/$repo/commit/$sha"
list[$kind]+="$line"$'\n'
[[ $kind == maint ]] && count_maint=$((count_maint + 1))
done < <(git log --reverse --no-merges --format='%H%x09%s%x09%b%x1e' "${prev:+$prev..}$target" | tr '\n\036' ' \n' | sed 's/^ //')
changes=''
for pair in "breaking:🚨 Breaking Changes" "feat:🚀 Features" "enh:🌟 Enhancements" "fix:🐛 Bug fixes" "docs:📚 Documentation"; do
kind="${pair%%:*}"
[[ -n ${list[$kind]:-} ]] || continue
changes+="### ${pair#*:}"$'\n'"${list[$kind]}"
done
if [[ -n ${list[maint]:-} ]]; then
changes+=$'\n'"<details><summary>🧰 Maintenance ($count_maint)</summary>"$'\n\n'"${list[maint]}"$'\n'"</details>"$'\n'
fi
if [[ -n $prev ]]; then
full="**Full Changelog**: https://github.com/$repo/compare/$prev...$tag"
else
full="**Full Changelog**: https://github.com/$repo/commits/$tag"
fi
# A release with highlights is a big one: it gets a heading and the support
# section, as in Immich. A patch is a sentence or two and the list.
if grep -q '^## Highlights$' <<< "$entry"; then
cat <<EOF
# 🚀 $name $tag
$entry
## ☕ Support $name
If $name is useful to you, you can buy me a coffee. It keeps these tools going. Found a bug or have an idea? Tell me in the [issues](https://github.com/$repo/issues).
<a href="https://ko-fi.com/felitendo"><img src="https://storage.ko-fi.com/cdn/kofi5.png?v=6" alt="Buy me a coffee on Ko-fi" height="48"></a>
----
EOF
else
printf '%s\n\n' "$entry"
fi
printf "## What's Changed\n%s\n\n%s\n" "$changes" "$full"
+30
View File
@@ -0,0 +1,30 @@
name: check
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
jobs:
check:
name: tests and shellcheck
runs-on: ubuntu-latest
# Arch has FreeRDP 3, which the hook test loads.
container: archlinux:latest
steps:
- name: Install the tools
run: |
pacman -Syu --noconfirm --needed git base-devel python freerdp shared-mime-info \
gettext scdoc shellcheck desktop-file-utils
- uses: actions/checkout@v7
- run: make check
# NTLM against the spec's test vectors, the feed, the .rdp files, the
# icons, a fake RD Web server, the hook and the command end to end.
- run: make test
- name: Build the catalogs and the man page
run: make build
+42
View File
@@ -0,0 +1,42 @@
name: release
on:
push:
tags: ['v*']
permissions:
contents: write
jobs:
release:
name: Test and release
runs-on: ubuntu-latest
container: archlinux:latest
steps:
- name: Install the tools
run: |
pacman -Syu --noconfirm --needed git base-devel python freerdp shared-mime-info \
gettext scdoc github-cli
- uses: actions/checkout@v7
with:
# All tags, for the link to the changes since the last release.
fetch-depth: 0
- name: Check the tag against the Makefile
run: .github/check-version.sh "${{ github.ref_name }}"
- name: Check CHANGELOG.md has this release
run: .github/release-notes.sh --title "${{ github.ref_name }}"
- run: make test
- name: Create the release
env:
GH_TOKEN: ${{ github.token }}
run: |
git config --global --add safe.directory "$PWD"
.github/release-notes.sh "${{ github.ref_name }}" > notes.md
gh release create "${{ github.ref_name }}" \
--title "$(.github/release-notes.sh --title "${{ github.ref_name }}")" \
--notes-file notes.md
+8
View File
@@ -0,0 +1,8 @@
# build artifacts
build/
po/*.mo
doc/*.1
__pycache__/
# packages
dist/
+24
View File
@@ -0,0 +1,24 @@
# Changelog
What each release brings, newest first. The [GitHub releases](https://github.com/LoonixTools/rdfeed/releases) add every commit that went into it.
## v1.0.0
_2026-10-02_
Welcome to the very first release of rdfeed! It brings the RemoteApps of your company to Linux: add the workspace once, and Word, Excel and the other apps show up in your app menu and open as windows of their own.
<p align="center">
<img width="480" alt="The rdfeed menu in Konsole with the Contoso Apps workspace" src="https://raw.githubusercontent.com/LoonixTools/rdfeed/v1.0.0/res/screenshots/menu.png">
</p>
### Highlights
- The apps of your company in the app menu
- Open your files in them with "Open with"
- Signs in like Windows, also with a work e-mail address
- Works with MFA on the gateway
- Sharp on scaled screens, with the right keyboard layout
- New apps show up by themselves every day
- The password stays in the keyring
- In 14 languages
+102
View File
@@ -0,0 +1,102 @@
# CLAUDE.md
## Style
- Keep everything short: replies, explanations, comments, docs.
- Use simple English: short sentences, common words.
- Avoid em dashes (—). Do not just swap them for "-" either. Rewrite the sentence instead, for
example with a comma, a colon, brackets or two sentences.
## Commits
- English only.
- Conventional Commits prefix: `feat:`, `fix:`, `chore:`, `docs:`, `refactor:`, `ci:`, `build:`.
- Subject as short as possible. Imperative, lowercase, no trailing period.
- Body only when something genuinely cannot be inferred from the diff.
- Never add `Co-Authored-By`, "Generated with" or any other AI attribution to commits or PR descriptions.
- Do not commit or push before I have checked the changes locally and said they are fine. Then
commit and push. Several attempts at the same thing make one commit, and attempts that did not
work make none. Different things done in one session get a commit each. This also goes for
releases and tags.
## Layout
- `src/rdfeed` and `src/lib/*.sh`: the command and its menu, plain bash.
- `src/fetch/rdfeed-fetch`: signs in to the feed (NTLM) and downloads the apps. Python, standard
library only.
- `src/hook/rdfeed-hook.c`: goes into FreeRDP with `LD_PRELOAD`. Waits before the second gateway
sign-in so the second MFA prompt comes, and keeps GFX off.
- `res/systemd`: the daily refresh, a user timer.
## Releases
Releases look like the ones of big projects such as Immich. `.github/release-notes.sh <tag>` builds
the notes: the entry from `CHANGELOG.md` (welcome and highlights), a support section, every commit
since the last release sorted by its prefix (`feat`, `fix`, `docs`, ...) with author and link, and
the full changelog link. So commit subjects end up in public: keep them clear.
1. Read `git log <last tag>..HEAD` and pick the version: only fixes → patch, something new → minor,
something that breaks or needs the user to act → major.
2. Bump the version and add the entry at the top of `CHANGELOG.md`, in one commit (`chore: 1.4.0`).
3. Push, then push the tag: `git tag v1.4.0 && git push origin main v1.4.0`. The `release` workflow
runs the tests and creates the release. It stops when the entry is missing.
4. PKGBUILDS picks up the new release for the AUR on its own.
A minor or major release (has `### Highlights`, gets a heading and the support section):
```markdown
## v1.4.0
_2026-09-24_
Welcome to rdfeed `v1.4.0`! One or two sentences on what this release is about.
<p align="center">
<img width="480" alt="What the picture shows" src="https://raw.githubusercontent.com/LoonixTools/rdfeed/v1.4.0/<path>">
</p>
### 🚨 Breaking changes
- Only if there are any: what changed, and what the user has to do.
### Highlights
- First highlight, a few words
- Second highlight
```
A picture under the welcome is optional. To show the menu or another screen of the program, use a
real screenshot of it running in Konsole, in English. Never a text copy of the screen. The same goes
for the README. The highlights stay a plain list: no heading or text per highlight, the list of
commits explains the rest.
A patch release is just a sentence or two, for example: "A small patch. The menu no longer closes
when you press Enter." The list of commits follows on its own.
- Write for users: what they notice, not how the code does it. Friendly and simple.
- Commands and settings they type go in backticks, buttons and labels in bold.
- To change an old release: edit its entry, commit, then
`gh release edit <tag> --title <tag> --notes "$(.github/release-notes.sh <tag>)"`.
## README
- New UI (a window, a screen, a menu, a setting, a notification) gets a screenshot in the README,
next to the text about it. Like for releases: a real screenshot of it running, in English.
- When a screen changes, take its screenshot again. The README never shows an old one.
- Screenshots show the demo workspace (Contoso Apps, `CONTOSO\jdoe`) from `tests/fake_rdweb.py`,
never a real company.
## Testing
Never test against the real setup: the workspaces, the keyring, the timer and the app menu belong
to the user's machine, and a real server counts every wrong password.
- `make test` runs NTLM against the test vectors of MS-NLMP, the feed, the .rdp files and the
icons, whole subscriptions against `tests/fake_rdweb.py`, the hook, and the command from `add` to
`remove`. No real server, no root.
- `tests/test_cli.sh` runs in a temporary home, with fakes for `secret-tool`, `systemctl`,
`notify-send`, `kbuildsycoca6` and `xfreerdp3` on `PATH`. Do the same for anything by hand.
- `python3 tests/fake_rdweb.py [port]` serves the demo workspace (user `jdoe`, domain `CONTOSO`,
the password is in the file). Point a temporary home at it with `XDG_CONFIG_HOME`,
`XDG_DATA_HOME`, `XDG_CACHE_HOME` and `RF_LIBDIR=src/lib RF_FETCH=src/fetch/rdfeed-fetch`.
- `make check` after every change. New strings: `po/update-pot.sh`, then translate them in every `po/*.po`.
+674
View File
@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
+146
View File
@@ -0,0 +1,146 @@
# rdfeed: build and install
#
# The shell front end and rdfeed-fetch install as they are. The one thing to
# compile is rdfeed-hook.so, which goes into FreeRDP. Translations and the
# man page are optional and skipped when msgfmt or scdoc are missing.
# Overridable so a packager can pass the version it is actually building
# (`make VERSION=$pkgver`). The literal below is the fallback for builds
# straight from a checkout, and is what a release tag has to carry.
VERSION ?= 1.0.0
PREFIX ?= /usr
DESTDIR ?=
BINDIR ?= $(PREFIX)/bin
DATADIR ?= $(PREFIX)/share
LIBDIR ?= $(DATADIR)/rdfeed/lib
LIBEXECDIR ?= $(PREFIX)/lib/rdfeed
LOCALEDIR ?= $(DATADIR)/locale
MANDIR ?= $(DATADIR)/man
ICONDIR ?= $(DATADIR)/icons/hicolor/scalable/apps
# Where systemd looks for user units, asked of systemd itself for a normal
# install. A build with a prefix of its own keeps them under that prefix.
ifeq ($(PREFIX),/usr)
USERUNITDIR ?= $(shell pkg-config --variable=systemduserunitdir systemd 2>/dev/null || echo /usr/lib/systemd/user)
else
USERUNITDIR ?= $(DATADIR)/systemd/user
endif
CC ?= cc
CFLAGS ?= -O2 -Wall -Wextra
BUILDDIR ?= build
HOOK := $(BUILDDIR)/rdfeed-hook.so
LINGUAS := de es fr it ja ko nl pl pt_BR ru tr uk zh_CN
MOFILES := $(patsubst %,po/%.mo,$(LINGUAS))
MANPAGE := doc/rdfeed.1
LIBS := $(wildcard src/lib/*.sh)
MSGFMT := $(shell command -v msgfmt 2>/dev/null)
SCDOC := $(shell command -v scdoc 2>/dev/null)
.PHONY: all build check test install uninstall clean version
all: build
build: $(HOOK) $(MOFILES) $(MANPAGE)
# The one place the version is written down, for everything that has to agree
# with it: the release workflow checking that the tag it was handed says the
# same thing, and the PKGBUILD.
version:
@echo $(VERSION)
$(HOOK): src/hook/rdfeed-hook.c
@mkdir -p $(BUILDDIR)
$(CC) $(CFLAGS) $(CPPFLAGS) -shared -fPIC $(LDFLAGS) -o $@ $< -ldl
po/%.mo: po/%.po
ifdef MSGFMT
$(MSGFMT) --check --output-file=$@ $<
else
@echo "msgfmt not found, skipping $@"
endif
$(MANPAGE): doc/rdfeed.1.scd
ifdef SCDOC
$(SCDOC) < $< > $@
else
@echo "scdoc not found, skipping $@"
endif
# Syntax-check every shell file and rdfeed-fetch, and run shellcheck when it
# is available. SC2034 is off: the files share their variables, and
# shellcheck looks at one file at a time.
check:
@set -e; for f in src/rdfeed $(LIBS) tests/*.sh; do \
bash -n "$$f" && echo "ok $$f"; \
done
@python3 -m py_compile src/fetch/rdfeed-fetch tests/*.py && echo "ok python"
@rm -rf src/fetch/__pycache__ tests/__pycache__
@if command -v shellcheck >/dev/null 2>&1; then \
shellcheck -x -e SC1090,SC1091,SC2034 src/rdfeed $(LIBS) tests/*.sh \
&& echo "ok shellcheck"; \
else \
echo "shellcheck not found, skipped"; \
fi
# The NTLM sign-in against the test vectors of the spec, the feed, the .rdp
# files and the icons, a whole subscription against a fake RD Web server, and
# the hook. Nothing here talks to a real server or touches the real setup.
test: $(HOOK)
python3 -m unittest discover -s tests -p 'test_*.py' -v
RDFEED_HOOK=$(HOOK) bash tests/test_hook.sh
bash tests/test_cli.sh
install: build
# the command
install -Dm755 src/rdfeed "$(DESTDIR)$(BINDIR)/rdfeed"
install -d "$(DESTDIR)$(LIBDIR)"
install -Dm644 -t "$(DESTDIR)$(LIBDIR)" $(LIBS)
sed -i -e 's|@VERSION@|$(VERSION)|g' \
-e 's|@LIBDIR@|$(LIBDIR)|g' \
-e 's|@LIBEXECDIR@|$(LIBEXECDIR)|g' \
-e 's|@LOCALEDIR@|$(LOCALEDIR)|g' \
"$(DESTDIR)$(BINDIR)/rdfeed" \
"$(DESTDIR)$(LIBDIR)"/*.sh
# the helpers
install -Dm755 src/fetch/rdfeed-fetch "$(DESTDIR)$(LIBEXECDIR)/rdfeed-fetch"
install -Dm755 $(HOOK) "$(DESTDIR)$(LIBEXECDIR)/rdfeed-hook.so"
# the daily refresh, and the icon of the workspace folders
install -Dm644 res/systemd/rdfeed-refresh.service "$(DESTDIR)$(USERUNITDIR)/rdfeed-refresh.service"
install -Dm644 res/systemd/rdfeed-refresh.timer "$(DESTDIR)$(USERUNITDIR)/rdfeed-refresh.timer"
sed -i -e 's|@BINDIR@|$(BINDIR)|g' "$(DESTDIR)$(USERUNITDIR)/rdfeed-refresh.service"
install -Dm644 res/rdfeed.svg "$(DESTDIR)$(ICONDIR)/rdfeed.svg"
# translations
@for l in $(LINGUAS); do \
if [ -f "po/$$l.mo" ]; then \
install -Dm644 "po/$$l.mo" \
"$(DESTDIR)$(LOCALEDIR)/$$l/LC_MESSAGES/rdfeed.mo"; \
fi; \
done
# documentation
@if [ -f $(MANPAGE) ]; then \
install -Dm644 $(MANPAGE) "$(DESTDIR)$(MANDIR)/man1/rdfeed.1"; \
fi
install -Dm644 README.md "$(DESTDIR)$(DATADIR)/doc/rdfeed/README.md"
uninstall:
rm -f "$(DESTDIR)$(BINDIR)/rdfeed"
rm -rf "$(DESTDIR)$(DATADIR)/rdfeed"
rm -rf "$(DESTDIR)$(LIBEXECDIR)"
rm -f "$(DESTDIR)$(USERUNITDIR)/rdfeed-refresh.service"
rm -f "$(DESTDIR)$(USERUNITDIR)/rdfeed-refresh.timer"
rm -f "$(DESTDIR)$(ICONDIR)/rdfeed.svg"
rm -f "$(DESTDIR)$(MANDIR)/man1/rdfeed.1"
rm -rf "$(DESTDIR)$(DATADIR)/doc/rdfeed"
@for l in $(LINGUAS); do rm -f "$(DESTDIR)$(LOCALEDIR)/$$l/LC_MESSAGES/rdfeed.mo"; done
clean:
rm -rf $(BUILDDIR) po/*.mo $(MANPAGE)
+152
View File
@@ -0,0 +1,152 @@
<p align="center">
<img width="200" src="res/rdfeed.svg" alt="rdfeed">
</p>
<h1 align="center">rdfeed</h1>
<h3 align="center">The RemoteApps of your company, in the Linux app menu.</h3>
<p align="center">
Word, Excel and the other Windows apps your company publishes open as windows of their own.<br>
Like "Work Resources" on Windows, through FreeRDP.
</p>
<h5 align="center">
<a href="#install">Install</a> |
<a href="#how-to-use">How to use</a> |
<a href="#is-it-safe">Is it safe?</a> |
<a href="https://github.com/LoonixTools/rdfeed/issues">Report a bug</a>
</h5>
<p align="center">
<a href="https://ko-fi.com/felitendo"><img src="https://storage.ko-fi.com/cdn/kofi5.png?v=6" alt="Buy me a coffee on Ko-fi" height="48"></a>
</p>
<p align="center">
<img src="res/screenshots/menu.png" alt="The rdfeed menu in Konsole: one workspace, Contoso Apps, with 4 apps for CONTOSO\jdoe" width="560">
</p>
## Install
<details>
<summary><b>Arch</b>, CachyOS, EndeavourOS, Manjaro</summary>
```bash
yay -S rdfeed
```
</details>
<details>
<summary>Other distributions</summary>
See [Build from source](#more). It needs FreeRDP 3, Python 3, bash and
`secret-tool` (libsecret).
</details>
## How to use
```bash
rdfeed
```
Press **1** and type the address your IT department gave you. The server
name or your work e-mail address often works too. Then sign in with your
Windows user name and password.
The apps show up in your app menu, in a folder with the name of the
workspace. Click one and it opens in a window of its own. If your company uses
MFA, confirm the sign-in in your authenticator app. Many servers ask twice: a
notification tells you when the second prompt is coming.
Files on your computer open in the apps too: right-click a `.docx` and pick
**Open with**, then Word.
<details>
<summary>Settings</summary>
<p align="center">
<img src="res/screenshots/settings.png" alt="The settings in Konsole: share a folder, open files with the apps, size, sound, graphics, the wait for the second MFA prompt, and a daily refresh, with the selected one explained at the bottom" width="640">
</p>
</details>
## Is it safe?
Your password goes into the keyring, not into a file. FreeRDP gets it through
a pipe, so it never shows up in the list of running programs.
The connection files from the server can ask to share every drive, camera and
USB device of your computer. rdfeed removes that. The apps only see the folder
you pick under **Share a folder**, and none by default. When you open a file in
an app, its folder is shared while that app runs.
## More
<details>
<summary>What it fixes in FreeRDP</summary>
Starting a RemoteApp with plain `xfreerdp3` often goes wrong. rdfeed takes care of it:
- **It hangs after the MFA prompt.** A connection broker needs a second sign-in
at the gateway. Its prompt only comes if the client waits about ten seconds,
and FreeRDP does not wait. rdfeed does.
- **Y and Z are swapped.** Under Wayland, FreeRDP sees the wrong keyboard layout.
rdfeed asks the desktop.
- **The apps are blurry or tiny** with a scaled desktop. rdfeed has the server
draw them at your scale.
- **Signing in to the feed fails.** It needs NTLM, which `curl` no longer
always has. rdfeed signs in by itself.
</details>
<details>
<summary>Known problems</summary>
- **A window stays partly empty after you minimize or resize it.** This is a
bug in FreeRDP. Under **Settings**, **Graphics** on classic paints every
window right, but some servers end the session with it.
- **A second app of the same workspace takes over the first one's connection.**
Its windows move into the new one, nothing closes.
</details>
<details>
<summary>How it works</summary>
| | |
|---|---|
| `rdfeed` | The menu and the commands. |
| `rdfeed-fetch` | Signs in to the feed and downloads the apps and their icons. |
| `rdfeed-hook.so` | Goes into FreeRDP: the wait for the second MFA prompt, and the classic graphics if you pick them. |
| `rdfeed-refresh.timer` | Loads the apps again once a day, so new ones show up. |
The feed is the one Windows subscribes to under *RemoteApp and Desktop
Connections* (`https://server/RDWeb/Feed/webfeed.aspx`). Azure Virtual Desktop
and Windows 365 sign in another way and are not supported. All details:
`man rdfeed`.
</details>
<details>
<summary>Build from source</summary>
```bash
make
make test
sudo make install
```
Needs a C compiler, and for the translations and the man page gettext and
scdoc. To run it: bash, Python 3, FreeRDP 3 (`xfreerdp3` or `xfreerdp`) and
`secret-tool`.
</details>
## Credits
- [FreeRDP](https://www.freerdp.com/), which does the actual work.
- [MS-NLMP](https://learn.microsoft.com/openspecs/windows_protocols/ms-nlmp/), for NTLM and its test vectors.
GPL-3.0-or-later.
+173
View File
@@ -0,0 +1,173 @@
rdfeed(1)
# NAME
rdfeed - the RemoteApps of your company in the Linux app menu
# SYNOPSIS
*rdfeed* [_command_]
# DESCRIPTION
Companies publish their Windows apps (Word, Excel, their own tools) as an RD
Web Access feed. Windows subscribes to it under "RemoteApp and Desktop
Connections" and puts the apps in the start menu, in a folder with the name of
the workspace. rdfeed does the same on Linux: each app gets an entry in the
app menu and opens as a window of its own, not as a whole Windows desktop.
The apps run on the company's servers, through FreeRDP.
Run without a command it shows an interactive menu. Everything it can be told
is reachable from there; the files behind it do not need to be edited by
hand.
# COMMANDS
*add* [_address_] [*--user* _name_]
Add a workspace. The address is the one of the feed, as Windows takes it
(*https://server/RDWeb/Feed/webfeed.aspx*), or just the server name, or
the RD Web Access page, or a work e-mail address. For an e-mail address
the feed is looked up in the TXT record *\_msradc.*_domain_, as Windows
does it. The user name can be _DOMAIN\\name_, _name@domain_, or just the
name, which takes the server's own domain.
Without a terminal, the user name and the password are read from stdin,
one per line.
*refresh* [_workspace_]
Load the apps of every workspace again, or of the one given. Apps that
were added show up in the app menu, apps that were taken away disappear.
*list*
The workspaces with their apps, each as _workspace/app_, the name *run*
takes.
*run* _app_ [_file_]
Start an app. _app_ is enough when only one workspace has an app of that
name, otherwise _workspace/app_. This is what the app menu entries run.
With a file, the app opens it. The folder of the file is shared as a
drive for that session, and the app is started with the path on it,
_\\\\tsclient\\folder\\file_, the way Windows does it. If the server
does not let the app take a file this way, it opens empty, and the file
is on that drive.
*remove* _workspace_
Remove a workspace: its app menu entries, its folder, the downloaded
files, the saved password.
*-h*, *--help*
Show the help.
*-V*, *--version*
Show the version.
# SETTINGS
*Share a folder*
Off, Documents, Downloads or the home folder. The apps see it as a drive
and can open and save files there. Off by default: then they only see the
files on the server.
*Open files with the apps*
On by default. The app menu entries name the file types of each app, so
file managers offer it under "Open with", for example Word for .docx
files. The types come from the feed when the administrator entered them,
as Windows takes them. Most feeds have none, and then rdfeed knows the
usual ones: Word, Excel, PowerPoint, OneNote, Outlook, Visio, Access,
Project, Publisher, PDF readers, AutoCAD and DWG TrueView, Notepad. Your
default apps stay as they are.
*Size*
How big the apps are drawn on the server. Automatic takes the scale your
desktop gives X11 programs that scale themselves (KDE Plasma's Xwayland
scale, or Xft.dpi), so the apps come out sharp and the right size.
*Sound*
Play the sound of the apps on this computer.
*Graphics*
GFX (the default) or classic. See *FREERDP PROBLEMS*.
*Wait for the second MFA prompt*
Automatic (the default), always or never. See *FREERDP PROBLEMS*.
*Refresh the apps every day*
A systemd user timer (*rdfeed-refresh.timer*) that loads the apps again
once a day, as Windows does. It is switched on with the first workspace.
When the password stops working it says so in a notification.
# FREERDP PROBLEMS
rdfeed starts each app with *xfreerdp3* (*xfreerdp* on Fedora) in RemoteApp
mode, with *rdfeed-hook.so* loaded into it. The hook works around two
problems that leave FreeRDP hanging or windows half painted:
*The second MFA prompt*
Most companies run a connection broker, which sends the client on to the
server that hosts the session. FreeRDP then opens a second tunnel through
the RD Gateway at once. With MFA on the gateway (the Azure MFA extension
of NPS), that second sign-in needs a second prompt in the authenticator
app. When it comes less than about ten seconds after the first one was
confirmed, no prompt is sent and the gateway waits forever. So the hook
waits 12 seconds before a new gateway connection, and a notification says
a second prompt is coming. Automatic waits only when the first sign-in
took long enough for somebody to confirm a prompt.
*Windows that stay empty*
In RemoteApp mode with the graphics pipeline (GFX), FreeRDP may not
repaint a window after it was minimized or resized. FreeRDP 3 has no
switch to turn GFX off. With *Graphics* on classic, the hook keeps it off
and every window paints right. But some servers end the session right
after signing in without GFX, so GFX stays the default. Not fixed yet.
# HOW SAFE IS THIS
The password goes into the keyring (the Secret Service, as GNOME Keyring or
KWallet provide it), never into a file. FreeRDP gets it on stdin, never on its
command line, so it shows in no process list.
The .rdp files from the server can ask to share every drive, camera and USB
device of this computer. rdfeed removes those lines. Only the folder picked
under *Share a folder* is shared, and the folder of a file you open in an app,
for as long as that session runs.
The server's certificate is trusted the first time and checked after that
(*/cert:tofu*). The feed itself is fetched over HTTPS with the system's
certificates.
# FILES
_~/.config/rdfeed/config_
The settings.
_~/.config/rdfeed/workspaces/_
One file per workspace: the address, the name, the user.
_~/.local/share/rdfeed/_
The apps of each workspace: the .rdp files and the icons.
_~/.local/share/applications/rdfeed-\*.desktop_
The app menu entries, with a folder per workspace in
_~/.config/menus/applications-merged/_.
_~/.cache/rdfeed/_
The log of the last start of each app, for when something goes wrong.
# LIMITS
Only feeds that sign in with Windows (NTLM) authentication, which is what RD
Web Access uses. Azure Virtual Desktop and Windows 365 sign in with Microsoft
Entra and are not supported.
Starting a second app of the same workspace opens a second connection, which
takes over the session of the first: its windows move into the new one.
Nothing closes.
# SEE ALSO
*xfreerdp3*(1), *secret-tool*(1)
# AUTHORS
Felitendo. Source and bug reports: https://github.com/LoonixTools/rdfeed
+564
View File
@@ -0,0 +1,564 @@
# German translation for rdfeed.
# Copyright (C) 2026 Felitendo
# This file is distributed under the same license as rdfeed.
#
msgid ""
msgstr ""
"Project-Id-Version: rdfeed 1.0.0\n"
"Report-Msgid-Bugs-To: https://github.com/LoonixTools/rdfeed/issues\n"
"POT-Creation-Date: 2026-10-02 10:29+0200\n"
"PO-Revision-Date: 2026-10-02 09:30+0200\n"
"Last-Translator: Felitendo\n"
"Language-Team: German\n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: src/rdfeed:79 src/rdfeed:103
#, sh-printf-format
msgid "Could not write to %s."
msgstr "Konnte nicht nach %s schreiben."
#: src/rdfeed:82
#, sh-printf-format
msgid "Signing in to %s…"
msgstr "Anmeldung bei %s…"
#: src/rdfeed:100
msgid ""
"The password could not be saved in the keyring, so each app asks for it."
msgstr ""
"Das Passwort konnte nicht im Schlüsselbund gespeichert werden, darum fragt "
"jede App danach."
#: src/rdfeed:111
#, sh-printf-format
msgid "1 app from %s is now in your app menu."
msgstr "1 App von %s ist jetzt in deinem Anwendungsmenü."
#: src/rdfeed:113
#, sh-printf-format
msgid "%d apps from %s are now in your app menu."
msgstr "%d Apps von %s sind jetzt in deinem Anwendungsmenü."
#: src/rdfeed:129
msgid ""
"Your IT department has the address. A work e-mail address often works too."
msgstr ""
"Die Adresse bekommst du von deiner IT-Abteilung. Oft klappt auch deine "
"Firmen-E-Mail-Adresse."
#: src/rdfeed:130
msgid "Feed address, server or e-mail:"
msgstr "Feed-Adresse, Server oder E-Mail:"
#: src/rdfeed:137
#, sh-printf-format
msgid "No feed is registered for %s. Ask your IT department for the address."
msgstr ""
"Für %s ist kein Feed hinterlegt. Frag deine IT-Abteilung nach der Adresse."
#: src/rdfeed:142 src/rdfeed:156
msgid "User name (DOMAIN\\name or name@domain):"
msgstr "Benutzername (DOMÄNE\\name oder name@domäne):"
#: src/rdfeed:146 src/rdfeed:159
msgid "Password:"
msgstr "Passwort:"
#: src/rdfeed:178 src/rdfeed:236
#, sh-printf-format
msgid "No workspace yet. Add one with `%s add`."
msgstr "Noch kein Arbeitsbereich. Füge einen mit `%s add` hinzu."
#: src/rdfeed:187 src/lib/launch.sh:220
#, sh-printf-format
msgid "No password saved for %s. Sign in again in rdfeed."
msgstr "Für %s ist kein Passwort gespeichert. Melde dich in rdfeed neu an."
#: src/rdfeed:191 src/lib/launch.sh:149
#, sh-printf-format
msgid "Password for %s:"
msgstr "Passwort für %s:"
#: src/rdfeed:195
#, sh-printf-format
msgid "Refreshing %s…"
msgstr "%s wird aktualisiert…"
#: src/rdfeed:205
msgid "The password no longer works. Sign in again in rdfeed."
msgstr "Das Passwort funktioniert nicht mehr. Melde dich in rdfeed neu an."
#: src/rdfeed:214
#, sh-printf-format
msgid "%s: %d apps"
msgstr "%s: %d Apps"
#: src/rdfeed:222
#, sh-printf-format
msgid "Which workspace? See `%s list` for the names."
msgstr "Welcher Arbeitsbereich? Die Namen zeigt `%s list`."
#: src/rdfeed:229
#, sh-printf-format
msgid "%s and its apps are removed."
msgstr "%s und seine Apps sind entfernt."
#: src/rdfeed:253
#, sh-printf-format
msgid "Which app? See `%s list`."
msgstr "Welche App? Siehe `%s list`."
#: src/rdfeed:268 src/lib/launch.sh:199
#, sh-printf-format
msgid "There is no app %s. See `%s list`."
msgstr "Es gibt keine App %s. Siehe `%s list`."
#: src/rdfeed:270
#, sh-printf-format
msgid "Several workspaces have %s. Write it as WORKSPACE/APP, see `%s list`."
msgstr ""
"Mehrere Arbeitsbereiche haben %s. Schreib es als ARBEITSBEREICH/APP, siehe "
"`%s list`."
#: src/rdfeed:278
msgid "Usage: rdfeed [command]"
msgstr "Aufruf: rdfeed [Befehl]"
#: src/rdfeed:280
msgid "Commands:"
msgstr "Befehle:"
#: src/rdfeed:281
msgid "Add the workspace of your company"
msgstr "Den Arbeitsbereich deiner Firma hinzufügen"
#: src/rdfeed:282
msgid "Load the apps again"
msgstr "Die Apps neu laden"
#: src/rdfeed:283
msgid "List the workspaces and their apps"
msgstr "Arbeitsbereiche und ihre Apps auflisten"
#: src/rdfeed:284
msgid "Start an app, or open a file with it"
msgstr "Eine App starten oder eine Datei damit öffnen"
#: src/rdfeed:285
msgid "Remove a workspace and its apps"
msgstr "Einen Arbeitsbereich mit seinen Apps entfernen"
#: src/rdfeed:286
msgid "Show this help"
msgstr "Diese Hilfe anzeigen"
#: src/rdfeed:287
msgid "Show the version"
msgstr "Die Version anzeigen"
#: src/rdfeed:289
msgid "Without a command an interactive menu is shown."
msgstr "Ohne Befehl wird ein interaktives Menü angezeigt."
#: src/rdfeed:313
#, sh-printf-format
msgid "Unknown command: %s"
msgstr "Unbekannter Befehl: %s"
#: src/lib/common.sh:174 src/lib/menu.sh:251
msgid "never"
msgstr "nie"
#: src/lib/common.sh:184
msgid "just now"
msgstr "gerade eben"
#: src/lib/common.sh:186
msgid "1 minute ago"
msgstr "vor 1 Minute"
#: src/lib/common.sh:189
#, sh-printf-format
msgid "%d minutes ago"
msgstr "vor %d Minuten"
#: src/lib/common.sh:191
msgid "1 hour ago"
msgstr "vor 1 Stunde"
#: src/lib/common.sh:194
#, sh-printf-format
msgid "%d hours ago"
msgstr "vor %d Stunden"
#: src/lib/common.sh:196
msgid "1 day ago"
msgstr "vor 1 Tag"
#: src/lib/common.sh:199
#, sh-printf-format
msgid "%d days ago"
msgstr "vor %d Tagen"
#: src/lib/launch.sh:169
msgid ""
"Your password has expired. Change it on Windows or on the RD Web page, then "
"sign in again in rdfeed."
msgstr ""
"Dein Passwort ist abgelaufen. Ändere es unter Windows oder auf der RD-Web-"
"Seite und melde dich dann in rdfeed neu an."
#: src/lib/launch.sh:171
msgid "Your account is locked. Ask your IT department."
msgstr "Dein Konto ist gesperrt. Frag deine IT-Abteilung."
#: src/lib/launch.sh:173
msgid "The server did not accept the password. Sign in again in rdfeed."
msgstr ""
"Der Server hat das Passwort nicht angenommen. Melde dich in rdfeed neu an."
#: src/lib/launch.sh:175
msgid "Could not reach the server. Check the internet connection."
msgstr "Der Server ist nicht erreichbar. Prüf die Internetverbindung."
#: src/lib/launch.sh:178
#, sh-printf-format
msgid "FreeRDP stopped: %s"
msgstr "FreeRDP hat aufgegeben: %s"
#: src/lib/launch.sh:205 src/lib/launch.sh:206
#, sh-printf-format
msgid "File not found: %s"
msgstr "Datei nicht gefunden: %s"
#: src/lib/launch.sh:212 src/lib/launch.sh:213
msgid ""
"FreeRDP 3 is missing. Install it to start the apps (the package is called "
"freerdp)."
msgstr ""
"FreeRDP 3 fehlt. Installiere es, um die Apps zu starten (das Paket heißt "
"freerdp)."
#: src/lib/launch.sh:277
#, sh-printf-format
msgid "Opening %s in %s"
msgstr "%s wird in %s geöffnet"
#: src/lib/launch.sh:278
#, sh-printf-format
msgid "If it does not open by itself, open it in the app from the drive %s."
msgstr ""
"Falls sie sich nicht von selbst öffnet, öffne sie in der App vom Laufwerk %s."
#: src/lib/launch.sh:280
#, sh-printf-format
msgid "Starting %s"
msgstr "%s wird gestartet"
#: src/lib/launch.sh:281
msgid ""
"If your company uses MFA, confirm the sign-in in your authenticator app."
msgstr ""
"Wenn deine Firma MFA nutzt, bestätige die Anmeldung in deiner Authenticator-"
"App."
#: src/lib/launch.sh:290
msgid "A second sign-in is coming"
msgstr "Gleich kommt eine zweite Anmeldung"
#: src/lib/launch.sh:291
#, sh-printf-format
msgid ""
"Confirm the second prompt in your authenticator app too. It comes in about "
"%d seconds."
msgstr ""
"Bestätige auch die zweite Anfrage in deiner Authenticator-App. Sie kommt in "
"etwa %d Sekunden."
#: src/lib/launch.sh:301
#, sh-printf-format
msgid "%s could not start"
msgstr "%s konnte nicht starten"
#: src/lib/launch.sh:303
#, sh-printf-format
msgid "Log: %s"
msgstr "Protokoll: %s"
#. TRANSLATORS: the letter after "[" is the key for yes. y works too.
#: src/lib/menu.sh:93
msgid "[y/N]"
msgstr "[j/N]"
#: src/lib/menu.sh:163
msgid "1 app"
msgstr "1 App"
#: src/lib/menu.sh:165
#, sh-printf-format
msgid "%d apps"
msgstr "%d Apps"
#: src/lib/menu.sh:190
msgid "No workspace yet. Add the one of your company with [1]."
msgstr "Noch kein Arbeitsbereich. Füge den deiner Firma mit [1] hinzu."
#: src/lib/menu.sh:233
msgid "off"
msgstr "aus"
#: src/lib/menu.sh:234
msgid "home folder"
msgstr "Persönlicher Ordner"
#: src/lib/menu.sh:237
msgid "Documents"
msgstr "Dokumente"
#: src/lib/menu.sh:238
msgid "Downloads"
msgstr "Downloads"
#: src/lib/menu.sh:243
msgid "not found"
msgstr "nicht gefunden"
#: src/lib/menu.sh:246
#, sh-printf-format
msgid "automatic (%d %%)"
msgstr "automatisch (%d %%)"
#: src/lib/menu.sh:248
msgid "classic"
msgstr "klassisch"
#: src/lib/menu.sh:249
msgid "GFX"
msgstr "GFX"
#: src/lib/menu.sh:250
msgid "always"
msgstr "immer"
#: src/lib/menu.sh:252
msgid "automatic"
msgstr "automatisch"
#: src/lib/menu.sh:261
msgid ""
"The apps only see the files on the server. Pick a folder to open and save "
"files of this computer in them."
msgstr ""
"Die Apps sehen nur die Dateien auf dem Server. Wähl einen Ordner, damit sie "
"Dateien dieses Computers öffnen und speichern können."
#: src/lib/menu.sh:262
msgid ""
"The apps can open and save files in this folder. It shows up as a drive in "
"them."
msgstr ""
"Die Apps können in diesem Ordner Dateien öffnen und speichern. Er erscheint "
"in ihnen als Laufwerk."
#: src/lib/menu.sh:263
msgid ""
"Offers the apps under \"Open with\" for their file types, for example Word "
"for .docx. The folder of the file is shared with the app while it is open."
msgstr ""
"Bietet die Apps unter „Öffnen mit“ für ihre Dateitypen an, zum Beispiel Word "
"für .docx. Der Ordner der Datei wird freigegeben, solange sie offen ist."
#: src/lib/menu.sh:264
msgid ""
"How big the apps are drawn. Automatic follows the scale of your desktop, so "
"they are sharp and the right size."
msgstr ""
"Wie groß die Apps gezeichnet werden. Automatisch folgt der Skalierung deines "
"Desktops, so sind sie scharf und richtig groß."
#: src/lib/menu.sh:265
msgid "Plays the sound of the apps here, for calls and videos."
msgstr "Spielt den Ton der Apps hier ab, für Anrufe und Videos."
#: src/lib/menu.sh:266
msgid ""
"Paints every window right. But some servers end the session right after "
"signing in, then go back to GFX."
msgstr "Zeichnet jedes Fenster richtig. Manche Server beenden die Sitzung aber direkt nach der Anmeldung, dann zurück auf GFX."
#: src/lib/menu.sh:267
msgid ""
"Works with every server. FreeRDP may not repaint a window after it was "
"minimized or resized."
msgstr "Geht mit jedem Server. FreeRDP zeichnet ein Fenster nach dem Minimieren oder Größe-Ändern eventuell nicht neu."
#: src/lib/menu.sh:268
msgid ""
"Always waits before the second sign-in. For when the second prompt does not "
"come with automatic."
msgstr ""
"Wartet immer vor der zweiten Anmeldung. Für den Fall, dass mit automatisch "
"keine zweite Anfrage kommt."
#: src/lib/menu.sh:269
msgid "Never waits. Right when your company has no MFA."
msgstr "Wartet nie. Richtig, wenn deine Firma kein MFA nutzt."
#: src/lib/menu.sh:270
msgid ""
"Many servers with MFA need a second sign-in, and its prompt only comes after "
"a short wait. Automatic waits when the first sign-in took a while."
msgstr ""
"Viele Server mit MFA brauchen eine zweite Anmeldung, und deren Anfrage kommt "
"erst nach kurzem Warten. Automatisch wartet, wenn die erste Anmeldung etwas "
"gedauert hat."
#: src/lib/menu.sh:273
msgid ""
"Loads the list of apps again once a day, so new apps show up by themselves."
msgstr ""
"Lädt die Liste der Apps einmal am Tag neu, so tauchen neue Apps von selbst "
"auf."
#: src/lib/menu.sh:275 src/lib/menu.sh:330
msgid "Needs a systemd user session."
msgstr "Braucht eine systemd-Benutzersitzung."
#: src/lib/menu.sh:322 src/lib/menu.sh:325
msgid "Could not save the setting."
msgstr "Einstellung konnte nicht gespeichert werden."
#: src/lib/menu.sh:371 src/lib/menu.sh:609
msgid "Settings"
msgstr "Einstellungen"
#: src/lib/menu.sh:372
msgid "↑↓ select ←→ or Space: change q: back"
msgstr "↑↓ auswählen ←→ oder Leertaste: ändern q: zurück"
#: src/lib/menu.sh:373
msgid "ON"
msgstr "AN"
#: src/lib/menu.sh:374
msgid "OFF"
msgstr "AUS"
#: src/lib/menu.sh:456 src/lib/menu.sh:608
msgid "Workspaces"
msgstr "Arbeitsbereiche"
#. TRANSLATORS: keep the letters, they are the keys.
#: src/lib/menu.sh:458
msgid ""
"↑↓ select r: refresh s: sign in again d: remove a: add q: back"
msgstr ""
"↑↓ auswählen r: aktualisieren s: neu anmelden d: entfernen a: "
"hinzufügen q: zurück"
#: src/lib/menu.sh:459
msgid "No workspace yet. Press a to add one."
msgstr "Noch kein Arbeitsbereich. Drück a, um einen hinzuzufügen."
#: src/lib/menu.sh:484
#, sh-printf-format
msgid "refreshed %s"
msgstr "aktualisiert %s"
#: src/lib/menu.sh:512
#, sh-printf-format
msgid "Remove \"%s\" and its apps?"
msgstr "„%s“ und seine Apps entfernen?"
#: src/lib/menu.sh:536 src/lib/menu.sh:607
msgid "Start an app"
msgstr "Eine App starten"
#: src/lib/menu.sh:537
msgid "↑↓ select Enter: start q: back"
msgstr "↑↓ auswählen Enter: starten q: zurück"
#: src/lib/menu.sh:538
msgid "No apps yet. Add a workspace first."
msgstr "Noch keine Apps. Füge zuerst einen Arbeitsbereich hinzu."
#: src/lib/menu.sh:579
#, sh-printf-format
msgid "%s is starting. It opens in a window of its own."
msgstr "%s startet. Es öffnet sich in einem eigenen Fenster."
#: src/lib/menu.sh:606
msgid "Add a workspace"
msgstr "Arbeitsbereich hinzufügen"
#: src/lib/menu.sh:610
msgid "Quit"
msgstr "Beenden"
#: src/lib/workspace.sh:163
msgid "The server did not accept the user name or password."
msgstr "Der Server hat Benutzername oder Passwort nicht angenommen."
#: src/lib/workspace.sh:164
#, sh-printf-format
msgid "Could not reach the server: %s"
msgstr "Server nicht erreichbar: %s"
#: src/lib/workspace.sh:165
#, sh-printf-format
msgid "There is no RD Web feed at %s."
msgstr "Unter %s gibt es keinen RD-Web-Feed."
#: src/lib/workspace.sh:166
#, sh-printf-format
msgid "Could not load the apps: %s"
msgstr "Die Apps konnten nicht geladen werden: %s"
#: src/lib/workspace.sh:282
#, sh-printf-format
msgid "Remote desktop of %s"
msgstr "Remotedesktop von %s"
#: src/lib/workspace.sh:284
#, sh-printf-format
msgid "%s from %s"
msgstr "%s von %s"
#: src/lib/menu.sh
msgid "Apps"
msgstr "Apps"
#: src/lib/menu.sh
msgid "Share a folder"
msgstr "Ordner freigeben"
#: src/lib/menu.sh
msgid "Open files with the apps"
msgstr "Dateien mit den Apps öffnen"
#: src/lib/menu.sh
msgid "Size"
msgstr "Größe"
#: src/lib/menu.sh
msgid "Sound"
msgstr "Ton"
#: src/lib/menu.sh
msgid "Graphics"
msgstr "Grafik"
#: src/lib/menu.sh
msgid "Signing in"
msgstr "Anmeldung"
#: src/lib/menu.sh
msgid "Wait for the second MFA prompt"
msgstr "Auf zweite MFA-Anfrage warten"
#: src/lib/menu.sh
msgid "Refresh the apps every day"
msgstr "Apps täglich aktualisieren"
+565
View File
@@ -0,0 +1,565 @@
# Spanish translation for rdfeed.
# Copyright (C) 2026 Felitendo
# This file is distributed under the same license as rdfeed.
#
msgid ""
msgstr ""
"Project-Id-Version: rdfeed 1.0.0\n"
"Report-Msgid-Bugs-To: https://github.com/LoonixTools/rdfeed/issues\n"
"POT-Creation-Date: 2026-10-02 10:29+0200\n"
"PO-Revision-Date: 2026-10-02 09:30+0200\n"
"Last-Translator: Felitendo\n"
"Language-Team: Spanish\n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: src/rdfeed:79 src/rdfeed:103
#, sh-printf-format
msgid "Could not write to %s."
msgstr "No se pudo escribir en %s."
#: src/rdfeed:82
#, sh-printf-format
msgid "Signing in to %s…"
msgstr "Iniciando sesión en %s…"
#: src/rdfeed:100
msgid ""
"The password could not be saved in the keyring, so each app asks for it."
msgstr ""
"No se pudo guardar la contraseña en el llavero, así que cada app la pedirá."
#: src/rdfeed:111
#, sh-printf-format
msgid "1 app from %s is now in your app menu."
msgstr "1 app de %s ya está en tu menú de aplicaciones."
#: src/rdfeed:113
#, sh-printf-format
msgid "%d apps from %s are now in your app menu."
msgstr "%d apps de %s ya están en tu menú de aplicaciones."
#: src/rdfeed:129
msgid ""
"Your IT department has the address. A work e-mail address often works too."
msgstr ""
"Tu departamento de TI tiene la dirección. A menudo también sirve tu correo "
"del trabajo."
#: src/rdfeed:130
msgid "Feed address, server or e-mail:"
msgstr "Dirección del feed, servidor o correo:"
#: src/rdfeed:137
#, sh-printf-format
msgid "No feed is registered for %s. Ask your IT department for the address."
msgstr ""
"No hay ningún feed registrado para %s. Pide la dirección a tu departamento "
"de TI."
#: src/rdfeed:142 src/rdfeed:156
msgid "User name (DOMAIN\\name or name@domain):"
msgstr "Nombre de usuario (DOMINIO\\nombre o nombre@dominio):"
#: src/rdfeed:146 src/rdfeed:159
msgid "Password:"
msgstr "Contraseña:"
#: src/rdfeed:178 src/rdfeed:236
#, sh-printf-format
msgid "No workspace yet. Add one with `%s add`."
msgstr "Todavía no hay ningún espacio de trabajo. Añade uno con `%s add`."
#: src/rdfeed:187 src/lib/launch.sh:220
#, sh-printf-format
msgid "No password saved for %s. Sign in again in rdfeed."
msgstr ""
"No hay ninguna contraseña guardada para %s. Vuelve a iniciar sesión en "
"rdfeed."
#: src/rdfeed:191 src/lib/launch.sh:149
#, sh-printf-format
msgid "Password for %s:"
msgstr "Contraseña de %s:"
#: src/rdfeed:195
#, sh-printf-format
msgid "Refreshing %s…"
msgstr "Actualizando %s…"
#: src/rdfeed:205
msgid "The password no longer works. Sign in again in rdfeed."
msgstr "La contraseña ya no funciona. Vuelve a iniciar sesión en rdfeed."
#: src/rdfeed:214
#, sh-printf-format
msgid "%s: %d apps"
msgstr "%s: %d apps"
#: src/rdfeed:222
#, sh-printf-format
msgid "Which workspace? See `%s list` for the names."
msgstr "¿Qué espacio de trabajo? Los nombres están en `%s list`."
#: src/rdfeed:229
#, sh-printf-format
msgid "%s and its apps are removed."
msgstr "%s y sus apps se han quitado."
#: src/rdfeed:253
#, sh-printf-format
msgid "Which app? See `%s list`."
msgstr "¿Qué app? Mira `%s list`."
#: src/rdfeed:268 src/lib/launch.sh:199
#, sh-printf-format
msgid "There is no app %s. See `%s list`."
msgstr "No existe la app %s. Mira `%s list`."
#: src/rdfeed:270
#, sh-printf-format
msgid "Several workspaces have %s. Write it as WORKSPACE/APP, see `%s list`."
msgstr ""
"Varios espacios de trabajo tienen %s. Escríbelo como ESPACIO/APP, mira `%s "
"list`."
#: src/rdfeed:278
msgid "Usage: rdfeed [command]"
msgstr "Uso: rdfeed [comando]"
#: src/rdfeed:280
msgid "Commands:"
msgstr "Comandos:"
#: src/rdfeed:281
msgid "Add the workspace of your company"
msgstr "Añadir el espacio de trabajo de tu empresa"
#: src/rdfeed:282
msgid "Load the apps again"
msgstr "Volver a cargar las apps"
#: src/rdfeed:283
msgid "List the workspaces and their apps"
msgstr "Listar los espacios de trabajo y sus apps"
#: src/rdfeed:284
msgid "Start an app, or open a file with it"
msgstr "Abrir una app, o un archivo con ella"
#: src/rdfeed:285
msgid "Remove a workspace and its apps"
msgstr "Quitar un espacio de trabajo y sus apps"
#: src/rdfeed:286
msgid "Show this help"
msgstr "Mostrar esta ayuda"
#: src/rdfeed:287
msgid "Show the version"
msgstr "Mostrar la versión"
#: src/rdfeed:289
msgid "Without a command an interactive menu is shown."
msgstr "Sin comando se muestra un menú interactivo."
#: src/rdfeed:313
#, sh-printf-format
msgid "Unknown command: %s"
msgstr "Comando desconocido: %s"
#: src/lib/common.sh:174 src/lib/menu.sh:251
msgid "never"
msgstr "nunca"
#: src/lib/common.sh:184
msgid "just now"
msgstr "ahora mismo"
#: src/lib/common.sh:186
msgid "1 minute ago"
msgstr "hace 1 minuto"
#: src/lib/common.sh:189
#, sh-printf-format
msgid "%d minutes ago"
msgstr "hace %d minutos"
#: src/lib/common.sh:191
msgid "1 hour ago"
msgstr "hace 1 hora"
#: src/lib/common.sh:194
#, sh-printf-format
msgid "%d hours ago"
msgstr "hace %d horas"
#: src/lib/common.sh:196
msgid "1 day ago"
msgstr "hace 1 día"
#: src/lib/common.sh:199
#, sh-printf-format
msgid "%d days ago"
msgstr "hace %d días"
#: src/lib/launch.sh:169
msgid ""
"Your password has expired. Change it on Windows or on the RD Web page, then "
"sign in again in rdfeed."
msgstr ""
"Tu contraseña ha caducado. Cámbiala en Windows o en la página de RD Web y "
"luego vuelve a iniciar sesión en rdfeed."
#: src/lib/launch.sh:171
msgid "Your account is locked. Ask your IT department."
msgstr "Tu cuenta está bloqueada. Pregunta a tu departamento de TI."
#: src/lib/launch.sh:173
msgid "The server did not accept the password. Sign in again in rdfeed."
msgstr ""
"El servidor no aceptó la contraseña. Vuelve a iniciar sesión en rdfeed."
#: src/lib/launch.sh:175
msgid "Could not reach the server. Check the internet connection."
msgstr "No se pudo conectar con el servidor. Comprueba la conexión a internet."
#: src/lib/launch.sh:178
#, sh-printf-format
msgid "FreeRDP stopped: %s"
msgstr "FreeRDP se detuvo: %s"
#: src/lib/launch.sh:205 src/lib/launch.sh:206
#, sh-printf-format
msgid "File not found: %s"
msgstr "No se encontró el archivo: %s"
#: src/lib/launch.sh:212 src/lib/launch.sh:213
msgid ""
"FreeRDP 3 is missing. Install it to start the apps (the package is called "
"freerdp)."
msgstr ""
"Falta FreeRDP 3. Instálalo para abrir las apps (el paquete se llama freerdp)."
#: src/lib/launch.sh:277
#, sh-printf-format
msgid "Opening %s in %s"
msgstr "Abriendo %s en %s"
#: src/lib/launch.sh:278
#, sh-printf-format
msgid "If it does not open by itself, open it in the app from the drive %s."
msgstr "Si no se abre solo, ábrelo en la app desde la unidad %s."
#: src/lib/launch.sh:280
#, sh-printf-format
msgid "Starting %s"
msgstr "Abriendo %s"
#: src/lib/launch.sh:281
msgid ""
"If your company uses MFA, confirm the sign-in in your authenticator app."
msgstr ""
"Si tu empresa usa MFA, confirma el inicio de sesión en tu app de "
"autenticación."
#: src/lib/launch.sh:290
msgid "A second sign-in is coming"
msgstr "Viene un segundo inicio de sesión"
#: src/lib/launch.sh:291
#, sh-printf-format
msgid ""
"Confirm the second prompt in your authenticator app too. It comes in about "
"%d seconds."
msgstr ""
"Confirma también la segunda solicitud en tu app de autenticación. Llega en "
"unos %d segundos."
#: src/lib/launch.sh:301
#, sh-printf-format
msgid "%s could not start"
msgstr "%s no se pudo abrir"
#: src/lib/launch.sh:303
#, sh-printf-format
msgid "Log: %s"
msgstr "Registro: %s"
#. TRANSLATORS: the letter after "[" is the key for yes. y works too.
#: src/lib/menu.sh:93
msgid "[y/N]"
msgstr "[s/N]"
#: src/lib/menu.sh:163
msgid "1 app"
msgstr "1 app"
#: src/lib/menu.sh:165
#, sh-printf-format
msgid "%d apps"
msgstr "%d apps"
#: src/lib/menu.sh:190
msgid "No workspace yet. Add the one of your company with [1]."
msgstr ""
"Todavía no hay ningún espacio de trabajo. Añade el de tu empresa con [1]."
#: src/lib/menu.sh:233
msgid "off"
msgstr "desactivado"
#: src/lib/menu.sh:234
msgid "home folder"
msgstr "carpeta personal"
#: src/lib/menu.sh:237
msgid "Documents"
msgstr "Documentos"
#: src/lib/menu.sh:238
msgid "Downloads"
msgstr "Descargas"
#: src/lib/menu.sh:243
msgid "not found"
msgstr "no encontrada"
#: src/lib/menu.sh:246
#, sh-printf-format
msgid "automatic (%d %%)"
msgstr "automático (%d %%)"
#: src/lib/menu.sh:248
msgid "classic"
msgstr "clásico"
#: src/lib/menu.sh:249
msgid "GFX"
msgstr "GFX"
#: src/lib/menu.sh:250
msgid "always"
msgstr "siempre"
#: src/lib/menu.sh:252
msgid "automatic"
msgstr "automático"
#: src/lib/menu.sh:261
msgid ""
"The apps only see the files on the server. Pick a folder to open and save "
"files of this computer in them."
msgstr ""
"Las apps solo ven los archivos del servidor. Elige una carpeta para que "
"puedan abrir y guardar archivos de este equipo."
#: src/lib/menu.sh:262
msgid ""
"The apps can open and save files in this folder. It shows up as a drive in "
"them."
msgstr ""
"Las apps pueden abrir y guardar archivos en esta carpeta. Aparece en ellas "
"como una unidad."
#: src/lib/menu.sh:263
msgid ""
"Offers the apps under \"Open with\" for their file types, for example Word "
"for .docx. The folder of the file is shared with the app while it is open."
msgstr ""
"Ofrece las apps en «Abrir con» para sus tipos de archivo, por ejemplo Word "
"para .docx. La carpeta del archivo se comparte mientras está abierto."
#: src/lib/menu.sh:264
msgid ""
"How big the apps are drawn. Automatic follows the scale of your desktop, so "
"they are sharp and the right size."
msgstr ""
"Qué tamaño tienen las apps. Automático sigue la escala de tu escritorio, así "
"se ven nítidas y del tamaño correcto."
#: src/lib/menu.sh:265
msgid "Plays the sound of the apps here, for calls and videos."
msgstr "Reproduce aquí el sonido de las apps, para llamadas y vídeos."
#: src/lib/menu.sh:266
msgid ""
"Paints every window right. But some servers end the session right after "
"signing in, then go back to GFX."
msgstr "Dibuja bien cada ventana. Pero algunos servidores cierran la sesión justo después de entrar; entonces vuelve a GFX."
#: src/lib/menu.sh:267
msgid ""
"Works with every server. FreeRDP may not repaint a window after it was "
"minimized or resized."
msgstr "Funciona con cualquier servidor. Puede que FreeRDP no vuelva a dibujar una ventana tras minimizarla o cambiar su tamaño."
#: src/lib/menu.sh:268
msgid ""
"Always waits before the second sign-in. For when the second prompt does not "
"come with automatic."
msgstr ""
"Espera siempre antes del segundo inicio de sesión. Para cuando con "
"automático no llega la segunda solicitud."
#: src/lib/menu.sh:269
msgid "Never waits. Right when your company has no MFA."
msgstr "Nunca espera. Lo adecuado si tu empresa no usa MFA."
#: src/lib/menu.sh:270
msgid ""
"Many servers with MFA need a second sign-in, and its prompt only comes after "
"a short wait. Automatic waits when the first sign-in took a while."
msgstr ""
"Muchos servidores con MFA piden un segundo inicio de sesión, y su solicitud "
"solo llega tras una breve espera. Automático espera si el primero tardó un "
"poco."
#: src/lib/menu.sh:273
msgid ""
"Loads the list of apps again once a day, so new apps show up by themselves."
msgstr ""
"Vuelve a cargar la lista de apps una vez al día, así las nuevas aparecen "
"solas."
#: src/lib/menu.sh:275 src/lib/menu.sh:330
msgid "Needs a systemd user session."
msgstr "Necesita una sesión de usuario de systemd."
#: src/lib/menu.sh:322 src/lib/menu.sh:325
msgid "Could not save the setting."
msgstr "No se pudo guardar el ajuste."
#: src/lib/menu.sh:371 src/lib/menu.sh:609
msgid "Settings"
msgstr "Ajustes"
#: src/lib/menu.sh:372
msgid "↑↓ select ←→ or Space: change q: back"
msgstr "↑↓ elegir ←→ o Espacio: cambiar q: volver"
#: src/lib/menu.sh:373
msgid "ON"
msgstr "ACTIVADO"
#: src/lib/menu.sh:374
msgid "OFF"
msgstr "DESACTIVADO"
#: src/lib/menu.sh:456 src/lib/menu.sh:608
msgid "Workspaces"
msgstr "Espacios de trabajo"
#. TRANSLATORS: keep the letters, they are the keys.
#: src/lib/menu.sh:458
msgid ""
"↑↓ select r: refresh s: sign in again d: remove a: add q: back"
msgstr ""
"↑↓ elegir r: actualizar s: volver a entrar d: quitar a: añadir q: "
"volver"
#: src/lib/menu.sh:459
msgid "No workspace yet. Press a to add one."
msgstr "Todavía no hay ningún espacio de trabajo. Pulsa a para añadir uno."
#: src/lib/menu.sh:484
#, sh-printf-format
msgid "refreshed %s"
msgstr "actualizado %s"
#: src/lib/menu.sh:512
#, sh-printf-format
msgid "Remove \"%s\" and its apps?"
msgstr "¿Quitar «%s» y sus apps?"
#: src/lib/menu.sh:536 src/lib/menu.sh:607
msgid "Start an app"
msgstr "Abrir una app"
#: src/lib/menu.sh:537
msgid "↑↓ select Enter: start q: back"
msgstr "↑↓ elegir Intro: abrir q: volver"
#: src/lib/menu.sh:538
msgid "No apps yet. Add a workspace first."
msgstr "Todavía no hay apps. Añade primero un espacio de trabajo."
#: src/lib/menu.sh:579
#, sh-printf-format
msgid "%s is starting. It opens in a window of its own."
msgstr "%s se está abriendo. Se abre en una ventana propia."
#: src/lib/menu.sh:606
msgid "Add a workspace"
msgstr "Añadir un espacio de trabajo"
#: src/lib/menu.sh:610
msgid "Quit"
msgstr "Salir"
#: src/lib/workspace.sh:163
msgid "The server did not accept the user name or password."
msgstr "El servidor no aceptó el nombre de usuario o la contraseña."
#: src/lib/workspace.sh:164
#, sh-printf-format
msgid "Could not reach the server: %s"
msgstr "No se pudo conectar con el servidor: %s"
#: src/lib/workspace.sh:165
#, sh-printf-format
msgid "There is no RD Web feed at %s."
msgstr "No hay ningún feed de RD Web en %s."
#: src/lib/workspace.sh:166
#, sh-printf-format
msgid "Could not load the apps: %s"
msgstr "No se pudieron cargar las apps: %s"
#: src/lib/workspace.sh:282
#, sh-printf-format
msgid "Remote desktop of %s"
msgstr "Escritorio remoto de %s"
#: src/lib/workspace.sh:284
#, sh-printf-format
msgid "%s from %s"
msgstr "%s de %s"
#: src/lib/menu.sh
msgid "Apps"
msgstr "Apps"
#: src/lib/menu.sh
msgid "Share a folder"
msgstr "Compartir una carpeta"
#: src/lib/menu.sh
msgid "Open files with the apps"
msgstr "Abrir archivos con las apps"
#: src/lib/menu.sh
msgid "Size"
msgstr "Tamaño"
#: src/lib/menu.sh
msgid "Sound"
msgstr "Sonido"
#: src/lib/menu.sh
msgid "Graphics"
msgstr "Gráficos"
#: src/lib/menu.sh
msgid "Signing in"
msgstr "Inicio de sesión"
#: src/lib/menu.sh
msgid "Wait for the second MFA prompt"
msgstr "Esperar la segunda solicitud de MFA"
#: src/lib/menu.sh
msgid "Refresh the apps every day"
msgstr "Actualizar las apps cada día"
+574
View File
@@ -0,0 +1,574 @@
# French translation for rdfeed.
# Copyright (C) 2026 Felitendo
# This file is distributed under the same license as rdfeed.
#
msgid ""
msgstr ""
"Project-Id-Version: rdfeed 1.0.0\n"
"Report-Msgid-Bugs-To: https://github.com/LoonixTools/rdfeed/issues\n"
"POT-Creation-Date: 2026-10-02 10:29+0200\n"
"PO-Revision-Date: 2026-10-02 09:30+0200\n"
"Last-Translator: Felitendo\n"
"Language-Team: French\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: src/rdfeed:79 src/rdfeed:103
#, sh-printf-format
msgid "Could not write to %s."
msgstr "Impossible d'écrire dans %s."
#: src/rdfeed:82
#, sh-printf-format
msgid "Signing in to %s…"
msgstr "Connexion à %s…"
#: src/rdfeed:100
msgid ""
"The password could not be saved in the keyring, so each app asks for it."
msgstr ""
"Le mot de passe n'a pas pu être enregistré dans le trousseau, chaque "
"application le demandera donc."
#: src/rdfeed:111
#, sh-printf-format
msgid "1 app from %s is now in your app menu."
msgstr "1 application de %s est maintenant dans votre menu des applications."
#: src/rdfeed:113
#, sh-printf-format
msgid "%d apps from %s are now in your app menu."
msgstr ""
"%d applications de %s sont maintenant dans votre menu des applications."
#: src/rdfeed:129
msgid ""
"Your IT department has the address. A work e-mail address often works too."
msgstr ""
"Votre service informatique connaît l'adresse. Souvent, votre adresse e-mail "
"professionnelle fonctionne aussi."
#: src/rdfeed:130
msgid "Feed address, server or e-mail:"
msgstr "Adresse du flux, serveur ou e-mail :"
#: src/rdfeed:137
#, sh-printf-format
msgid "No feed is registered for %s. Ask your IT department for the address."
msgstr ""
"Aucun flux n'est enregistré pour %s. Demandez l'adresse à votre service "
"informatique."
#: src/rdfeed:142 src/rdfeed:156
msgid "User name (DOMAIN\\name or name@domain):"
msgstr "Nom d'utilisateur (DOMAINE\\nom ou nom@domaine) :"
#: src/rdfeed:146 src/rdfeed:159
msgid "Password:"
msgstr "Mot de passe :"
#: src/rdfeed:178 src/rdfeed:236
#, sh-printf-format
msgid "No workspace yet. Add one with `%s add`."
msgstr "Aucun espace de travail pour l'instant. Ajoutez-en un avec `%s add`."
#: src/rdfeed:187 src/lib/launch.sh:220
#, sh-printf-format
msgid "No password saved for %s. Sign in again in rdfeed."
msgstr "Aucun mot de passe enregistré pour %s. Reconnectez-vous dans rdfeed."
#: src/rdfeed:191 src/lib/launch.sh:149
#, sh-printf-format
msgid "Password for %s:"
msgstr "Mot de passe pour %s :"
#: src/rdfeed:195
#, sh-printf-format
msgid "Refreshing %s…"
msgstr "Actualisation de %s…"
#: src/rdfeed:205
msgid "The password no longer works. Sign in again in rdfeed."
msgstr "Le mot de passe ne fonctionne plus. Reconnectez-vous dans rdfeed."
#: src/rdfeed:214
#, sh-printf-format
msgid "%s: %d apps"
msgstr "%s : %d applications"
#: src/rdfeed:222
#, sh-printf-format
msgid "Which workspace? See `%s list` for the names."
msgstr "Quel espace de travail ? Les noms sont dans `%s list`."
#: src/rdfeed:229
#, sh-printf-format
msgid "%s and its apps are removed."
msgstr "%s et ses applications ont été supprimés."
#: src/rdfeed:253
#, sh-printf-format
msgid "Which app? See `%s list`."
msgstr "Quelle application ? Voir `%s list`."
#: src/rdfeed:268 src/lib/launch.sh:199
#, sh-printf-format
msgid "There is no app %s. See `%s list`."
msgstr "L'application %s n'existe pas. Voir `%s list`."
#: src/rdfeed:270
#, sh-printf-format
msgid "Several workspaces have %s. Write it as WORKSPACE/APP, see `%s list`."
msgstr ""
"Plusieurs espaces de travail ont %s. Écrivez-le sous la forme ESPACE/"
"APPLICATION, voir `%s list`."
#: src/rdfeed:278
msgid "Usage: rdfeed [command]"
msgstr "Utilisation : rdfeed [commande]"
#: src/rdfeed:280
msgid "Commands:"
msgstr "Commandes :"
#: src/rdfeed:281
msgid "Add the workspace of your company"
msgstr "Ajouter l'espace de travail de votre entreprise"
#: src/rdfeed:282
msgid "Load the apps again"
msgstr "Recharger les applications"
#: src/rdfeed:283
msgid "List the workspaces and their apps"
msgstr "Lister les espaces de travail et leurs applications"
#: src/rdfeed:284
msgid "Start an app, or open a file with it"
msgstr "Lancer une application, ou ouvrir un fichier avec"
#: src/rdfeed:285
msgid "Remove a workspace and its apps"
msgstr "Supprimer un espace de travail et ses applications"
#: src/rdfeed:286
msgid "Show this help"
msgstr "Afficher cette aide"
#: src/rdfeed:287
msgid "Show the version"
msgstr "Afficher la version"
#: src/rdfeed:289
msgid "Without a command an interactive menu is shown."
msgstr "Sans commande, un menu interactif s'affiche."
#: src/rdfeed:313
#, sh-printf-format
msgid "Unknown command: %s"
msgstr "Commande inconnue : %s"
#: src/lib/common.sh:174 src/lib/menu.sh:251
msgid "never"
msgstr "jamais"
#: src/lib/common.sh:184
msgid "just now"
msgstr "à l'instant"
#: src/lib/common.sh:186
msgid "1 minute ago"
msgstr "il y a 1 minute"
#: src/lib/common.sh:189
#, sh-printf-format
msgid "%d minutes ago"
msgstr "il y a %d minutes"
#: src/lib/common.sh:191
msgid "1 hour ago"
msgstr "il y a 1 heure"
#: src/lib/common.sh:194
#, sh-printf-format
msgid "%d hours ago"
msgstr "il y a %d heures"
#: src/lib/common.sh:196
msgid "1 day ago"
msgstr "il y a 1 jour"
#: src/lib/common.sh:199
#, sh-printf-format
msgid "%d days ago"
msgstr "il y a %d jours"
#: src/lib/launch.sh:169
msgid ""
"Your password has expired. Change it on Windows or on the RD Web page, then "
"sign in again in rdfeed."
msgstr ""
"Votre mot de passe a expiré. Changez-le sous Windows ou sur la page RD Web, "
"puis reconnectez-vous dans rdfeed."
#: src/lib/launch.sh:171
msgid "Your account is locked. Ask your IT department."
msgstr ""
"Votre compte est verrouillé. Adressez-vous à votre service informatique."
#: src/lib/launch.sh:173
msgid "The server did not accept the password. Sign in again in rdfeed."
msgstr ""
"Le serveur n'a pas accepté le mot de passe. Reconnectez-vous dans rdfeed."
#: src/lib/launch.sh:175
msgid "Could not reach the server. Check the internet connection."
msgstr "Impossible de joindre le serveur. Vérifiez la connexion internet."
#: src/lib/launch.sh:178
#, sh-printf-format
msgid "FreeRDP stopped: %s"
msgstr "FreeRDP s'est arrêté : %s"
#: src/lib/launch.sh:205 src/lib/launch.sh:206
#, sh-printf-format
msgid "File not found: %s"
msgstr "Fichier introuvable : %s"
#: src/lib/launch.sh:212 src/lib/launch.sh:213
msgid ""
"FreeRDP 3 is missing. Install it to start the apps (the package is called "
"freerdp)."
msgstr ""
"FreeRDP 3 est absent. Installez-le pour lancer les applications (le paquet "
"s'appelle freerdp)."
#: src/lib/launch.sh:277
#, sh-printf-format
msgid "Opening %s in %s"
msgstr "Ouverture de %s dans %s"
#: src/lib/launch.sh:278
#, sh-printf-format
msgid "If it does not open by itself, open it in the app from the drive %s."
msgstr ""
"S'il ne s'ouvre pas tout seul, ouvrez-le dans l'application depuis le "
"lecteur %s."
#: src/lib/launch.sh:280
#, sh-printf-format
msgid "Starting %s"
msgstr "Lancement de %s"
#: src/lib/launch.sh:281
msgid ""
"If your company uses MFA, confirm the sign-in in your authenticator app."
msgstr ""
"Si votre entreprise utilise la MFA, confirmez la connexion dans votre "
"application d'authentification."
#: src/lib/launch.sh:290
msgid "A second sign-in is coming"
msgstr "Une deuxième connexion arrive"
#: src/lib/launch.sh:291
#, sh-printf-format
msgid ""
"Confirm the second prompt in your authenticator app too. It comes in about "
"%d seconds."
msgstr ""
"Confirmez aussi la deuxième demande dans votre application "
"d'authentification. Elle arrive dans environ %d secondes."
#: src/lib/launch.sh:301
#, sh-printf-format
msgid "%s could not start"
msgstr "%s n'a pas pu démarrer"
#: src/lib/launch.sh:303
#, sh-printf-format
msgid "Log: %s"
msgstr "Journal : %s"
#. TRANSLATORS: the letter after "[" is the key for yes. y works too.
#: src/lib/menu.sh:93
msgid "[y/N]"
msgstr "[o/N]"
#: src/lib/menu.sh:163
msgid "1 app"
msgstr "1 application"
#: src/lib/menu.sh:165
#, sh-printf-format
msgid "%d apps"
msgstr "%d applications"
#: src/lib/menu.sh:190
msgid "No workspace yet. Add the one of your company with [1]."
msgstr ""
"Aucun espace de travail pour l'instant. Ajoutez celui de votre entreprise "
"avec [1]."
#: src/lib/menu.sh:233
msgid "off"
msgstr "désactivé"
#: src/lib/menu.sh:234
msgid "home folder"
msgstr "dossier personnel"
#: src/lib/menu.sh:237
msgid "Documents"
msgstr "Documents"
#: src/lib/menu.sh:238
msgid "Downloads"
msgstr "Téléchargements"
#: src/lib/menu.sh:243
msgid "not found"
msgstr "introuvable"
#: src/lib/menu.sh:246
#, sh-printf-format
msgid "automatic (%d %%)"
msgstr "automatique (%d %%)"
#: src/lib/menu.sh:248
msgid "classic"
msgstr "classique"
#: src/lib/menu.sh:249
msgid "GFX"
msgstr "GFX"
#: src/lib/menu.sh:250
msgid "always"
msgstr "toujours"
#: src/lib/menu.sh:252
msgid "automatic"
msgstr "automatique"
#: src/lib/menu.sh:261
msgid ""
"The apps only see the files on the server. Pick a folder to open and save "
"files of this computer in them."
msgstr ""
"Les applications ne voient que les fichiers du serveur. Choisissez un "
"dossier pour qu'elles puissent ouvrir et enregistrer des fichiers de cet "
"ordinateur."
#: src/lib/menu.sh:262
msgid ""
"The apps can open and save files in this folder. It shows up as a drive in "
"them."
msgstr ""
"Les applications peuvent ouvrir et enregistrer des fichiers dans ce dossier. "
"Il y apparaît comme un lecteur."
#: src/lib/menu.sh:263
msgid ""
"Offers the apps under \"Open with\" for their file types, for example Word "
"for .docx. The folder of the file is shared with the app while it is open."
msgstr ""
"Propose les applications dans « Ouvrir avec » pour leurs types de fichiers, "
"par exemple Word pour .docx. Le dossier du fichier est partagé tant qu'il "
"est ouvert."
#: src/lib/menu.sh:264
msgid ""
"How big the apps are drawn. Automatic follows the scale of your desktop, so "
"they are sharp and the right size."
msgstr ""
"La taille à laquelle les applications sont dessinées. Automatique suit "
"l'échelle de votre bureau : elles sont nettes et à la bonne taille."
#: src/lib/menu.sh:265
msgid "Plays the sound of the apps here, for calls and videos."
msgstr "Joue ici le son des applications, pour les appels et les vidéos."
#: src/lib/menu.sh:266
msgid ""
"Paints every window right. But some servers end the session right after "
"signing in, then go back to GFX."
msgstr "Dessine correctement chaque fenêtre. Mais certains serveurs ferment la session juste après la connexion : revenez alors à GFX."
#: src/lib/menu.sh:267
msgid ""
"Works with every server. FreeRDP may not repaint a window after it was "
"minimized or resized."
msgstr "Fonctionne avec tous les serveurs. FreeRDP peut ne pas redessiner une fenêtre réduite ou redimensionnée."
#: src/lib/menu.sh:268
msgid ""
"Always waits before the second sign-in. For when the second prompt does not "
"come with automatic."
msgstr ""
"Attend toujours avant la deuxième connexion. Si la deuxième demande n'arrive "
"pas en automatique."
#: src/lib/menu.sh:269
msgid "Never waits. Right when your company has no MFA."
msgstr ""
"N'attend jamais. Le bon choix si votre entreprise n'utilise pas la MFA."
#: src/lib/menu.sh:270
msgid ""
"Many servers with MFA need a second sign-in, and its prompt only comes after "
"a short wait. Automatic waits when the first sign-in took a while."
msgstr ""
"Beaucoup de serveurs avec MFA demandent une deuxième connexion, dont la "
"demande n'arrive qu'après une courte attente. Automatique attend si la "
"première a pris du temps."
#: src/lib/menu.sh:273
msgid ""
"Loads the list of apps again once a day, so new apps show up by themselves."
msgstr ""
"Recharge la liste des applications une fois par jour : les nouvelles "
"apparaissent d'elles-mêmes."
#: src/lib/menu.sh:275 src/lib/menu.sh:330
msgid "Needs a systemd user session."
msgstr "Nécessite une session utilisateur systemd."
#: src/lib/menu.sh:322 src/lib/menu.sh:325
msgid "Could not save the setting."
msgstr "Impossible d'enregistrer le réglage."
#: src/lib/menu.sh:371 src/lib/menu.sh:609
msgid "Settings"
msgstr "Réglages"
#: src/lib/menu.sh:372
msgid "↑↓ select ←→ or Space: change q: back"
msgstr "↑↓ choisir ←→ ou Espace : changer q : retour"
#: src/lib/menu.sh:373
msgid "ON"
msgstr "ACTIVÉ"
#: src/lib/menu.sh:374
msgid "OFF"
msgstr "DÉSACTIVÉ"
#: src/lib/menu.sh:456 src/lib/menu.sh:608
msgid "Workspaces"
msgstr "Espaces de travail"
#. TRANSLATORS: keep the letters, they are the keys.
#: src/lib/menu.sh:458
msgid ""
"↑↓ select r: refresh s: sign in again d: remove a: add q: back"
msgstr ""
"↑↓ choisir r : actualiser s : se reconnecter d : supprimer a : "
"ajouter q : retour"
#: src/lib/menu.sh:459
msgid "No workspace yet. Press a to add one."
msgstr ""
"Aucun espace de travail pour l'instant. Appuyez sur a pour en ajouter un."
#: src/lib/menu.sh:484
#, sh-printf-format
msgid "refreshed %s"
msgstr "actualisé %s"
#: src/lib/menu.sh:512
#, sh-printf-format
msgid "Remove \"%s\" and its apps?"
msgstr "Supprimer « %s » et ses applications ?"
#: src/lib/menu.sh:536 src/lib/menu.sh:607
msgid "Start an app"
msgstr "Lancer une application"
#: src/lib/menu.sh:537
msgid "↑↓ select Enter: start q: back"
msgstr "↑↓ choisir Entrée : lancer q : retour"
#: src/lib/menu.sh:538
msgid "No apps yet. Add a workspace first."
msgstr "Pas encore d'applications. Ajoutez d'abord un espace de travail."
#: src/lib/menu.sh:579
#, sh-printf-format
msgid "%s is starting. It opens in a window of its own."
msgstr "%s démarre. L'application s'ouvre dans sa propre fenêtre."
#: src/lib/menu.sh:606
msgid "Add a workspace"
msgstr "Ajouter un espace de travail"
#: src/lib/menu.sh:610
msgid "Quit"
msgstr "Quitter"
#: src/lib/workspace.sh:163
msgid "The server did not accept the user name or password."
msgstr "Le serveur n'a pas accepté le nom d'utilisateur ou le mot de passe."
#: src/lib/workspace.sh:164
#, sh-printf-format
msgid "Could not reach the server: %s"
msgstr "Impossible de joindre le serveur : %s"
#: src/lib/workspace.sh:165
#, sh-printf-format
msgid "There is no RD Web feed at %s."
msgstr "Il n'y a pas de flux RD Web à l'adresse %s."
#: src/lib/workspace.sh:166
#, sh-printf-format
msgid "Could not load the apps: %s"
msgstr "Impossible de charger les applications : %s"
#: src/lib/workspace.sh:282
#, sh-printf-format
msgid "Remote desktop of %s"
msgstr "Bureau à distance de %s"
#: src/lib/workspace.sh:284
#, sh-printf-format
msgid "%s from %s"
msgstr "%s de %s"
#: src/lib/menu.sh
msgid "Apps"
msgstr "Applications"
#: src/lib/menu.sh
msgid "Share a folder"
msgstr "Partager un dossier"
#: src/lib/menu.sh
msgid "Open files with the apps"
msgstr "Ouvrir des fichiers avec les applications"
#: src/lib/menu.sh
msgid "Size"
msgstr "Taille"
#: src/lib/menu.sh
msgid "Sound"
msgstr "Son"
#: src/lib/menu.sh
msgid "Graphics"
msgstr "Graphismes"
#: src/lib/menu.sh
msgid "Signing in"
msgstr "Connexion"
#: src/lib/menu.sh
msgid "Wait for the second MFA prompt"
msgstr "Attendre la deuxième demande MFA"
#: src/lib/menu.sh
msgid "Refresh the apps every day"
msgstr "Actualiser les applications chaque jour"
+561
View File
@@ -0,0 +1,561 @@
# Italian translation for rdfeed.
# Copyright (C) 2026 Felitendo
# This file is distributed under the same license as rdfeed.
#
msgid ""
msgstr ""
"Project-Id-Version: rdfeed 1.0.0\n"
"Report-Msgid-Bugs-To: https://github.com/LoonixTools/rdfeed/issues\n"
"POT-Creation-Date: 2026-10-02 10:29+0200\n"
"PO-Revision-Date: 2026-10-02 09:30+0200\n"
"Last-Translator: Felitendo\n"
"Language-Team: Italian\n"
"Language: it\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: src/rdfeed:79 src/rdfeed:103
#, sh-printf-format
msgid "Could not write to %s."
msgstr "Impossibile scrivere in %s."
#: src/rdfeed:82
#, sh-printf-format
msgid "Signing in to %s…"
msgstr "Accesso a %s…"
#: src/rdfeed:100
msgid ""
"The password could not be saved in the keyring, so each app asks for it."
msgstr ""
"Non è stato possibile salvare la password nel portachiavi, quindi ogni app "
"la chiederà."
#: src/rdfeed:111
#, sh-printf-format
msgid "1 app from %s is now in your app menu."
msgstr "1 app di %s è ora nel menu delle applicazioni."
#: src/rdfeed:113
#, sh-printf-format
msgid "%d apps from %s are now in your app menu."
msgstr "%d app di %s sono ora nel menu delle applicazioni."
#: src/rdfeed:129
msgid ""
"Your IT department has the address. A work e-mail address often works too."
msgstr ""
"L'indirizzo lo ha il tuo reparto IT. Spesso funziona anche l'e-mail di "
"lavoro."
#: src/rdfeed:130
msgid "Feed address, server or e-mail:"
msgstr "Indirizzo del feed, server o e-mail:"
#: src/rdfeed:137
#, sh-printf-format
msgid "No feed is registered for %s. Ask your IT department for the address."
msgstr "Nessun feed registrato per %s. Chiedi l'indirizzo al tuo reparto IT."
#: src/rdfeed:142 src/rdfeed:156
msgid "User name (DOMAIN\\name or name@domain):"
msgstr "Nome utente (DOMINIO\\nome o nome@dominio):"
#: src/rdfeed:146 src/rdfeed:159
msgid "Password:"
msgstr "Password:"
#: src/rdfeed:178 src/rdfeed:236
#, sh-printf-format
msgid "No workspace yet. Add one with `%s add`."
msgstr "Nessuno spazio di lavoro ancora. Aggiungine uno con `%s add`."
#: src/rdfeed:187 src/lib/launch.sh:220
#, sh-printf-format
msgid "No password saved for %s. Sign in again in rdfeed."
msgstr "Nessuna password salvata per %s. Accedi di nuovo in rdfeed."
#: src/rdfeed:191 src/lib/launch.sh:149
#, sh-printf-format
msgid "Password for %s:"
msgstr "Password per %s:"
#: src/rdfeed:195
#, sh-printf-format
msgid "Refreshing %s…"
msgstr "Aggiornamento di %s…"
#: src/rdfeed:205
msgid "The password no longer works. Sign in again in rdfeed."
msgstr "La password non funziona più. Accedi di nuovo in rdfeed."
#: src/rdfeed:214
#, sh-printf-format
msgid "%s: %d apps"
msgstr "%s: %d app"
#: src/rdfeed:222
#, sh-printf-format
msgid "Which workspace? See `%s list` for the names."
msgstr "Quale spazio di lavoro? I nomi sono in `%s list`."
#: src/rdfeed:229
#, sh-printf-format
msgid "%s and its apps are removed."
msgstr "%s e le sue app sono stati rimossi."
#: src/rdfeed:253
#, sh-printf-format
msgid "Which app? See `%s list`."
msgstr "Quale app? Vedi `%s list`."
#: src/rdfeed:268 src/lib/launch.sh:199
#, sh-printf-format
msgid "There is no app %s. See `%s list`."
msgstr "Non esiste l'app %s. Vedi `%s list`."
#: src/rdfeed:270
#, sh-printf-format
msgid "Several workspaces have %s. Write it as WORKSPACE/APP, see `%s list`."
msgstr ""
"Più spazi di lavoro hanno %s. Scrivilo come SPAZIO/APP, vedi `%s list`."
#: src/rdfeed:278
msgid "Usage: rdfeed [command]"
msgstr "Uso: rdfeed [comando]"
#: src/rdfeed:280
msgid "Commands:"
msgstr "Comandi:"
#: src/rdfeed:281
msgid "Add the workspace of your company"
msgstr "Aggiungi lo spazio di lavoro della tua azienda"
#: src/rdfeed:282
msgid "Load the apps again"
msgstr "Ricarica le app"
#: src/rdfeed:283
msgid "List the workspaces and their apps"
msgstr "Elenca gli spazi di lavoro e le loro app"
#: src/rdfeed:284
msgid "Start an app, or open a file with it"
msgstr "Avvia un'app, o apri un file con essa"
#: src/rdfeed:285
msgid "Remove a workspace and its apps"
msgstr "Rimuovi uno spazio di lavoro e le sue app"
#: src/rdfeed:286
msgid "Show this help"
msgstr "Mostra questo aiuto"
#: src/rdfeed:287
msgid "Show the version"
msgstr "Mostra la versione"
#: src/rdfeed:289
msgid "Without a command an interactive menu is shown."
msgstr "Senza comando viene mostrato un menu interattivo."
#: src/rdfeed:313
#, sh-printf-format
msgid "Unknown command: %s"
msgstr "Comando sconosciuto: %s"
#: src/lib/common.sh:174 src/lib/menu.sh:251
msgid "never"
msgstr "mai"
#: src/lib/common.sh:184
msgid "just now"
msgstr "proprio ora"
#: src/lib/common.sh:186
msgid "1 minute ago"
msgstr "1 minuto fa"
#: src/lib/common.sh:189
#, sh-printf-format
msgid "%d minutes ago"
msgstr "%d minuti fa"
#: src/lib/common.sh:191
msgid "1 hour ago"
msgstr "1 ora fa"
#: src/lib/common.sh:194
#, sh-printf-format
msgid "%d hours ago"
msgstr "%d ore fa"
#: src/lib/common.sh:196
msgid "1 day ago"
msgstr "1 giorno fa"
#: src/lib/common.sh:199
#, sh-printf-format
msgid "%d days ago"
msgstr "%d giorni fa"
#: src/lib/launch.sh:169
msgid ""
"Your password has expired. Change it on Windows or on the RD Web page, then "
"sign in again in rdfeed."
msgstr ""
"La password è scaduta. Cambiala in Windows o sulla pagina RD Web, poi accedi "
"di nuovo in rdfeed."
#: src/lib/launch.sh:171
msgid "Your account is locked. Ask your IT department."
msgstr "Il tuo account è bloccato. Chiedi al reparto IT."
#: src/lib/launch.sh:173
msgid "The server did not accept the password. Sign in again in rdfeed."
msgstr "Il server non ha accettato la password. Accedi di nuovo in rdfeed."
#: src/lib/launch.sh:175
msgid "Could not reach the server. Check the internet connection."
msgstr ""
"Impossibile raggiungere il server. Controlla la connessione a internet."
#: src/lib/launch.sh:178
#, sh-printf-format
msgid "FreeRDP stopped: %s"
msgstr "FreeRDP si è fermato: %s"
#: src/lib/launch.sh:205 src/lib/launch.sh:206
#, sh-printf-format
msgid "File not found: %s"
msgstr "File non trovato: %s"
#: src/lib/launch.sh:212 src/lib/launch.sh:213
msgid ""
"FreeRDP 3 is missing. Install it to start the apps (the package is called "
"freerdp)."
msgstr ""
"Manca FreeRDP 3. Installalo per avviare le app (il pacchetto si chiama "
"freerdp)."
#: src/lib/launch.sh:277
#, sh-printf-format
msgid "Opening %s in %s"
msgstr "Apertura di %s in %s"
#: src/lib/launch.sh:278
#, sh-printf-format
msgid "If it does not open by itself, open it in the app from the drive %s."
msgstr "Se non si apre da solo, aprilo nell'app dall'unità %s."
#: src/lib/launch.sh:280
#, sh-printf-format
msgid "Starting %s"
msgstr "Avvio di %s"
#: src/lib/launch.sh:281
msgid ""
"If your company uses MFA, confirm the sign-in in your authenticator app."
msgstr ""
"Se la tua azienda usa l'MFA, conferma l'accesso nell'app di autenticazione."
#: src/lib/launch.sh:290
msgid "A second sign-in is coming"
msgstr "Sta arrivando un secondo accesso"
#: src/lib/launch.sh:291
#, sh-printf-format
msgid ""
"Confirm the second prompt in your authenticator app too. It comes in about "
"%d seconds."
msgstr ""
"Conferma anche la seconda richiesta nell'app di autenticazione. Arriva tra "
"circa %d secondi."
#: src/lib/launch.sh:301
#, sh-printf-format
msgid "%s could not start"
msgstr "Impossibile avviare %s"
#: src/lib/launch.sh:303
#, sh-printf-format
msgid "Log: %s"
msgstr "Log: %s"
#. TRANSLATORS: the letter after "[" is the key for yes. y works too.
#: src/lib/menu.sh:93
msgid "[y/N]"
msgstr "[s/N]"
#: src/lib/menu.sh:163
msgid "1 app"
msgstr "1 app"
#: src/lib/menu.sh:165
#, sh-printf-format
msgid "%d apps"
msgstr "%d app"
#: src/lib/menu.sh:190
msgid "No workspace yet. Add the one of your company with [1]."
msgstr ""
"Nessuno spazio di lavoro ancora. Aggiungi quello della tua azienda con [1]."
#: src/lib/menu.sh:233
msgid "off"
msgstr "disattivo"
#: src/lib/menu.sh:234
msgid "home folder"
msgstr "cartella home"
#: src/lib/menu.sh:237
msgid "Documents"
msgstr "Documenti"
#: src/lib/menu.sh:238
msgid "Downloads"
msgstr "Download"
#: src/lib/menu.sh:243
msgid "not found"
msgstr "non trovata"
#: src/lib/menu.sh:246
#, sh-printf-format
msgid "automatic (%d %%)"
msgstr "automatica (%d %%)"
#: src/lib/menu.sh:248
msgid "classic"
msgstr "classico"
#: src/lib/menu.sh:249
msgid "GFX"
msgstr "GFX"
#: src/lib/menu.sh:250
msgid "always"
msgstr "sempre"
#: src/lib/menu.sh:252
msgid "automatic"
msgstr "automatico"
#: src/lib/menu.sh:261
msgid ""
"The apps only see the files on the server. Pick a folder to open and save "
"files of this computer in them."
msgstr ""
"Le app vedono solo i file sul server. Scegli una cartella perché possano "
"aprire e salvare file di questo computer."
#: src/lib/menu.sh:262
msgid ""
"The apps can open and save files in this folder. It shows up as a drive in "
"them."
msgstr ""
"Le app possono aprire e salvare file in questa cartella. Compare in esse "
"come un'unità."
#: src/lib/menu.sh:263
msgid ""
"Offers the apps under \"Open with\" for their file types, for example Word "
"for .docx. The folder of the file is shared with the app while it is open."
msgstr ""
"Offre le app in «Apri con» per i loro tipi di file, per esempio Word "
"per .docx. La cartella del file viene condivisa finché è aperto."
#: src/lib/menu.sh:264
msgid ""
"How big the apps are drawn. Automatic follows the scale of your desktop, so "
"they are sharp and the right size."
msgstr ""
"Quanto grandi vengono disegnate le app. Automatica segue la scala del "
"desktop, così sono nitide e della giusta dimensione."
#: src/lib/menu.sh:265
msgid "Plays the sound of the apps here, for calls and videos."
msgstr "Riproduce qui l'audio delle app, per chiamate e video."
#: src/lib/menu.sh:266
msgid ""
"Paints every window right. But some servers end the session right after "
"signing in, then go back to GFX."
msgstr "Disegna bene ogni finestra. Ma alcuni server chiudono la sessione subito dopo l'accesso: allora torna a GFX."
#: src/lib/menu.sh:267
msgid ""
"Works with every server. FreeRDP may not repaint a window after it was "
"minimized or resized."
msgstr "Funziona con ogni server. FreeRDP potrebbe non ridisegnare una finestra dopo averla ridotta o ridimensionata."
#: src/lib/menu.sh:268
msgid ""
"Always waits before the second sign-in. For when the second prompt does not "
"come with automatic."
msgstr ""
"Attende sempre prima del secondo accesso. Per quando con automatico la "
"seconda richiesta non arriva."
#: src/lib/menu.sh:269
msgid "Never waits. Right when your company has no MFA."
msgstr "Non attende mai. Giusto se la tua azienda non usa l'MFA."
#: src/lib/menu.sh:270
msgid ""
"Many servers with MFA need a second sign-in, and its prompt only comes after "
"a short wait. Automatic waits when the first sign-in took a while."
msgstr ""
"Molti server con MFA chiedono un secondo accesso, e la sua richiesta arriva "
"solo dopo una breve attesa. Automatico attende se il primo accesso ha "
"richiesto un po'."
#: src/lib/menu.sh:273
msgid ""
"Loads the list of apps again once a day, so new apps show up by themselves."
msgstr ""
"Ricarica l'elenco delle app una volta al giorno, così quelle nuove compaiono "
"da sole."
#: src/lib/menu.sh:275 src/lib/menu.sh:330
msgid "Needs a systemd user session."
msgstr "Serve una sessione utente di systemd."
#: src/lib/menu.sh:322 src/lib/menu.sh:325
msgid "Could not save the setting."
msgstr "Impossibile salvare l'impostazione."
#: src/lib/menu.sh:371 src/lib/menu.sh:609
msgid "Settings"
msgstr "Impostazioni"
#: src/lib/menu.sh:372
msgid "↑↓ select ←→ or Space: change q: back"
msgstr "↑↓ scegli ←→ o Spazio: cambia q: indietro"
#: src/lib/menu.sh:373
msgid "ON"
msgstr "ATTIVO"
#: src/lib/menu.sh:374
msgid "OFF"
msgstr "DISATTIVO"
#: src/lib/menu.sh:456 src/lib/menu.sh:608
msgid "Workspaces"
msgstr "Spazi di lavoro"
#. TRANSLATORS: keep the letters, they are the keys.
#: src/lib/menu.sh:458
msgid ""
"↑↓ select r: refresh s: sign in again d: remove a: add q: back"
msgstr ""
"↑↓ scegli r: aggiorna s: accedi di nuovo d: rimuovi a: aggiungi q: "
"indietro"
#: src/lib/menu.sh:459
msgid "No workspace yet. Press a to add one."
msgstr "Nessuno spazio di lavoro ancora. Premi a per aggiungerne uno."
#: src/lib/menu.sh:484
#, sh-printf-format
msgid "refreshed %s"
msgstr "aggiornato %s"
#: src/lib/menu.sh:512
#, sh-printf-format
msgid "Remove \"%s\" and its apps?"
msgstr "Rimuovere «%s» e le sue app?"
#: src/lib/menu.sh:536 src/lib/menu.sh:607
msgid "Start an app"
msgstr "Avvia un'app"
#: src/lib/menu.sh:537
msgid "↑↓ select Enter: start q: back"
msgstr "↑↓ scegli Invio: avvia q: indietro"
#: src/lib/menu.sh:538
msgid "No apps yet. Add a workspace first."
msgstr "Nessuna app ancora. Aggiungi prima uno spazio di lavoro."
#: src/lib/menu.sh:579
#, sh-printf-format
msgid "%s is starting. It opens in a window of its own."
msgstr "%s si sta avviando. Si apre in una finestra propria."
#: src/lib/menu.sh:606
msgid "Add a workspace"
msgstr "Aggiungi uno spazio di lavoro"
#: src/lib/menu.sh:610
msgid "Quit"
msgstr "Esci"
#: src/lib/workspace.sh:163
msgid "The server did not accept the user name or password."
msgstr "Il server non ha accettato il nome utente o la password."
#: src/lib/workspace.sh:164
#, sh-printf-format
msgid "Could not reach the server: %s"
msgstr "Impossibile raggiungere il server: %s"
#: src/lib/workspace.sh:165
#, sh-printf-format
msgid "There is no RD Web feed at %s."
msgstr "Non c'è nessun feed RD Web in %s."
#: src/lib/workspace.sh:166
#, sh-printf-format
msgid "Could not load the apps: %s"
msgstr "Impossibile caricare le app: %s"
#: src/lib/workspace.sh:282
#, sh-printf-format
msgid "Remote desktop of %s"
msgstr "Desktop remoto di %s"
#: src/lib/workspace.sh:284
#, sh-printf-format
msgid "%s from %s"
msgstr "%s di %s"
#: src/lib/menu.sh
msgid "Apps"
msgstr "App"
#: src/lib/menu.sh
msgid "Share a folder"
msgstr "Condividi una cartella"
#: src/lib/menu.sh
msgid "Open files with the apps"
msgstr "Apri i file con le app"
#: src/lib/menu.sh
msgid "Size"
msgstr "Dimensione"
#: src/lib/menu.sh
msgid "Sound"
msgstr "Audio"
#: src/lib/menu.sh
msgid "Graphics"
msgstr "Grafica"
#: src/lib/menu.sh
msgid "Signing in"
msgstr "Accesso"
#: src/lib/menu.sh
msgid "Wait for the second MFA prompt"
msgstr "Attendi la seconda richiesta MFA"
#: src/lib/menu.sh
msgid "Refresh the apps every day"
msgstr "Aggiorna le app ogni giorno"
+561
View File
@@ -0,0 +1,561 @@
# Japanese translation for rdfeed.
# Copyright (C) 2026 Felitendo
# This file is distributed under the same license as rdfeed.
#
msgid ""
msgstr ""
"Project-Id-Version: rdfeed 1.0.0\n"
"Report-Msgid-Bugs-To: https://github.com/LoonixTools/rdfeed/issues\n"
"POT-Creation-Date: 2026-10-02 10:29+0200\n"
"PO-Revision-Date: 2026-10-02 09:30+0200\n"
"Last-Translator: Felitendo\n"
"Language-Team: Japanese\n"
"Language: ja\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: src/rdfeed:79 src/rdfeed:103
#, sh-printf-format
msgid "Could not write to %s."
msgstr "%s に書き込めませんでした。"
#: src/rdfeed:82
#, sh-printf-format
msgid "Signing in to %s…"
msgstr "%s にサインインしています…"
#: src/rdfeed:100
msgid ""
"The password could not be saved in the keyring, so each app asks for it."
msgstr ""
"パスワードをキーリングに保存できなかったため、アプリごとに入力を求めます。"
#: src/rdfeed:111
#, sh-printf-format
msgid "1 app from %s is now in your app menu."
msgstr "%s のアプリ 1 個がアプリメニューに追加されました。"
#: src/rdfeed:113
#, sh-printf-format
msgid "%d apps from %s are now in your app menu."
msgstr "%d 個のアプリ(%s)がアプリメニューに追加されました。"
#: src/rdfeed:129
msgid ""
"Your IT department has the address. A work e-mail address often works too."
msgstr ""
"アドレスは IT 部門が知っています。仕事用のメールアドレスでも使えることがよく"
"あります。"
#: src/rdfeed:130
msgid "Feed address, server or e-mail:"
msgstr "フィードのアドレス、サーバー、またはメール:"
#: src/rdfeed:137
#, sh-printf-format
msgid "No feed is registered for %s. Ask your IT department for the address."
msgstr ""
"%s にはフィードが登録されていません。IT 部門にアドレスを確認してください。"
#: src/rdfeed:142 src/rdfeed:156
msgid "User name (DOMAIN\\name or name@domain):"
msgstr "ユーザー名(DOMAIN\\name または name@domain):"
#: src/rdfeed:146 src/rdfeed:159
msgid "Password:"
msgstr "パスワード:"
#: src/rdfeed:178 src/rdfeed:236
#, sh-printf-format
msgid "No workspace yet. Add one with `%s add`."
msgstr "ワークスペースはまだありません。`%s add` で追加してください。"
#: src/rdfeed:187 src/lib/launch.sh:220
#, sh-printf-format
msgid "No password saved for %s. Sign in again in rdfeed."
msgstr ""
"%s のパスワードが保存されていません。rdfeed でもう一度サインインしてくださ"
"い。"
#: src/rdfeed:191 src/lib/launch.sh:149
#, sh-printf-format
msgid "Password for %s:"
msgstr "%s のパスワード:"
#: src/rdfeed:195
#, sh-printf-format
msgid "Refreshing %s…"
msgstr "%s を更新しています…"
#: src/rdfeed:205
msgid "The password no longer works. Sign in again in rdfeed."
msgstr ""
"パスワードが使えなくなりました。rdfeed でもう一度サインインしてください。"
#: src/rdfeed:214
#, sh-printf-format
msgid "%s: %d apps"
msgstr "%s: アプリ %d 個"
#: src/rdfeed:222
#, sh-printf-format
msgid "Which workspace? See `%s list` for the names."
msgstr "どのワークスペースですか?名前は `%s list` で確認できます。"
#: src/rdfeed:229
#, sh-printf-format
msgid "%s and its apps are removed."
msgstr "%s とそのアプリを削除しました。"
#: src/rdfeed:253
#, sh-printf-format
msgid "Which app? See `%s list`."
msgstr "どのアプリですか?`%s list` を参照してください。"
#: src/rdfeed:268 src/lib/launch.sh:199
#, sh-printf-format
msgid "There is no app %s. See `%s list`."
msgstr "アプリ %s はありません。`%s list` を参照してください。"
#: src/rdfeed:270
#, sh-printf-format
msgid "Several workspaces have %s. Write it as WORKSPACE/APP, see `%s list`."
msgstr ""
"複数のワークスペースに %s があります。ワークスペース/アプリ の形で指定してく"
"ださい(`%s list` を参照)。"
#: src/rdfeed:278
msgid "Usage: rdfeed [command]"
msgstr "使い方: rdfeed [コマンド]"
#: src/rdfeed:280
msgid "Commands:"
msgstr "コマンド:"
#: src/rdfeed:281
msgid "Add the workspace of your company"
msgstr "会社のワークスペースを追加する"
#: src/rdfeed:282
msgid "Load the apps again"
msgstr "アプリを読み込み直す"
#: src/rdfeed:283
msgid "List the workspaces and their apps"
msgstr "ワークスペースとそのアプリを一覧表示する"
#: src/rdfeed:284
msgid "Start an app, or open a file with it"
msgstr "アプリを起動する、またはファイルを開く"
#: src/rdfeed:285
msgid "Remove a workspace and its apps"
msgstr "ワークスペースとそのアプリを削除する"
#: src/rdfeed:286
msgid "Show this help"
msgstr "このヘルプを表示"
#: src/rdfeed:287
msgid "Show the version"
msgstr "バージョンを表示"
#: src/rdfeed:289
msgid "Without a command an interactive menu is shown."
msgstr "コマンドなしで起動すると、対話式メニューが表示されます。"
#: src/rdfeed:313
#, sh-printf-format
msgid "Unknown command: %s"
msgstr "不明なコマンド: %s"
#: src/lib/common.sh:174 src/lib/menu.sh:251
msgid "never"
msgstr "なし"
#: src/lib/common.sh:184
msgid "just now"
msgstr "たった今"
#: src/lib/common.sh:186
msgid "1 minute ago"
msgstr "1 分前"
#: src/lib/common.sh:189
#, sh-printf-format
msgid "%d minutes ago"
msgstr "%d 分前"
#: src/lib/common.sh:191
msgid "1 hour ago"
msgstr "1 時間前"
#: src/lib/common.sh:194
#, sh-printf-format
msgid "%d hours ago"
msgstr "%d 時間前"
#: src/lib/common.sh:196
msgid "1 day ago"
msgstr "1 日前"
#: src/lib/common.sh:199
#, sh-printf-format
msgid "%d days ago"
msgstr "%d 日前"
#: src/lib/launch.sh:169
msgid ""
"Your password has expired. Change it on Windows or on the RD Web page, then "
"sign in again in rdfeed."
msgstr ""
"パスワードの有効期限が切れています。Windows か RD Web のページで変更してか"
"ら、rdfeed でもう一度サインインしてください。"
#: src/lib/launch.sh:171
msgid "Your account is locked. Ask your IT department."
msgstr "アカウントがロックされています。IT 部門に問い合わせてください。"
#: src/lib/launch.sh:173
msgid "The server did not accept the password. Sign in again in rdfeed."
msgstr ""
"サーバーがパスワードを受け付けませんでした。rdfeed でもう一度サインインしてく"
"ださい。"
#: src/lib/launch.sh:175
msgid "Could not reach the server. Check the internet connection."
msgstr "サーバーに接続できませんでした。インターネット接続を確認してください。"
#: src/lib/launch.sh:178
#, sh-printf-format
msgid "FreeRDP stopped: %s"
msgstr "FreeRDP が停止しました: %s"
#: src/lib/launch.sh:205 src/lib/launch.sh:206
#, sh-printf-format
msgid "File not found: %s"
msgstr "ファイルが見つかりません: %s"
#: src/lib/launch.sh:212 src/lib/launch.sh:213
msgid ""
"FreeRDP 3 is missing. Install it to start the apps (the package is called "
"freerdp)."
msgstr ""
"FreeRDP 3 がありません。アプリを起動するにはインストールしてください(パッ"
"ケージ名は freerdp)。"
#: src/lib/launch.sh:277
#, sh-printf-format
msgid "Opening %s in %s"
msgstr "%s を %s で開いています"
#: src/lib/launch.sh:278
#, sh-printf-format
msgid "If it does not open by itself, open it in the app from the drive %s."
msgstr "自動で開かない場合は、アプリでドライブ %s から開いてください。"
#: src/lib/launch.sh:280
#, sh-printf-format
msgid "Starting %s"
msgstr "%s を起動しています"
#: src/lib/launch.sh:281
msgid ""
"If your company uses MFA, confirm the sign-in in your authenticator app."
msgstr ""
"会社が MFA を使っている場合は、認証アプリでサインインを承認してください。"
#: src/lib/launch.sh:290
msgid "A second sign-in is coming"
msgstr "2 回目のサインインが来ます"
#: src/lib/launch.sh:291
#, sh-printf-format
msgid ""
"Confirm the second prompt in your authenticator app too. It comes in about "
"%d seconds."
msgstr "認証アプリで 2 回目の確認も承認してください。約 %d 秒後に届きます。"
#: src/lib/launch.sh:301
#, sh-printf-format
msgid "%s could not start"
msgstr "%s を起動できませんでした"
#: src/lib/launch.sh:303
#, sh-printf-format
msgid "Log: %s"
msgstr "ログ: %s"
#. TRANSLATORS: the letter after "[" is the key for yes. y works too.
#: src/lib/menu.sh:93
msgid "[y/N]"
msgstr "[y/N]"
#: src/lib/menu.sh:163
msgid "1 app"
msgstr "アプリ 1 個"
#: src/lib/menu.sh:165
#, sh-printf-format
msgid "%d apps"
msgstr "アプリ %d 個"
#: src/lib/menu.sh:190
msgid "No workspace yet. Add the one of your company with [1]."
msgstr ""
"ワークスペースはまだありません。[1] で会社のワークスペースを追加してくださ"
"い。"
#: src/lib/menu.sh:233
msgid "off"
msgstr "オフ"
#: src/lib/menu.sh:234
msgid "home folder"
msgstr "ホームフォルダー"
#: src/lib/menu.sh:237
msgid "Documents"
msgstr "ドキュメント"
#: src/lib/menu.sh:238
msgid "Downloads"
msgstr "ダウンロード"
#: src/lib/menu.sh:243
msgid "not found"
msgstr "見つかりません"
#: src/lib/menu.sh:246
#, sh-printf-format
msgid "automatic (%d %%)"
msgstr "自動 (%d %%)"
#: src/lib/menu.sh:248
msgid "classic"
msgstr "クラシック"
#: src/lib/menu.sh:249
msgid "GFX"
msgstr "GFX"
#: src/lib/menu.sh:250
msgid "always"
msgstr "常に"
#: src/lib/menu.sh:252
msgid "automatic"
msgstr "自動"
#: src/lib/menu.sh:261
msgid ""
"The apps only see the files on the server. Pick a folder to open and save "
"files of this computer in them."
msgstr ""
"アプリはサーバー上のファイルしか見えません。フォルダーを選ぶと、このコン"
"ピューターのファイルを開いたり保存したりできます。"
#: src/lib/menu.sh:262
msgid ""
"The apps can open and save files in this folder. It shows up as a drive in "
"them."
msgstr ""
"アプリはこのフォルダーのファイルを開いたり保存したりできます。アプリ内ではド"
"ライブとして表示されます。"
#: src/lib/menu.sh:263
msgid ""
"Offers the apps under \"Open with\" for their file types, for example Word "
"for .docx. The folder of the file is shared with the app while it is open."
msgstr ""
"ファイルの種類ごとに「アプリケーションで開く」にアプリを表示します"
"(例: .docx なら Word)。ファイルを開いている間、そのフォルダーが共有されま"
"す。"
#: src/lib/menu.sh:264
msgid ""
"How big the apps are drawn. Automatic follows the scale of your desktop, so "
"they are sharp and the right size."
msgstr ""
"アプリを描く大きさです。自動ではデスクトップの拡大率に合わせるので、くっきり"
"と適切な大きさになります。"
#: src/lib/menu.sh:265
msgid "Plays the sound of the apps here, for calls and videos."
msgstr "アプリの音をこのコンピューターで再生します。通話や動画に。"
#: src/lib/menu.sh:266
msgid ""
"Paints every window right. But some servers end the session right after "
"signing in, then go back to GFX."
msgstr "すべてのウィンドウを正しく描きます。ただしサインイン直後にセッションを切るサーバーもあり、その場合は GFX に戻してください。"
#: src/lib/menu.sh:267
msgid ""
"Works with every server. FreeRDP may not repaint a window after it was "
"minimized or resized."
msgstr "どのサーバーでも動きます。最小化やサイズ変更の後、FreeRDP がウィンドウを描き直さないことがあります。"
#: src/lib/menu.sh:268
msgid ""
"Always waits before the second sign-in. For when the second prompt does not "
"come with automatic."
msgstr ""
"2 回目のサインインの前に必ず待ちます。自動で 2 回目の確認が来ないときに。"
#: src/lib/menu.sh:269
msgid "Never waits. Right when your company has no MFA."
msgstr "待ちません。会社が MFA を使っていないとき向けです。"
#: src/lib/menu.sh:270
msgid ""
"Many servers with MFA need a second sign-in, and its prompt only comes after "
"a short wait. Automatic waits when the first sign-in took a while."
msgstr ""
"MFA を使う多くのサーバーでは 2 回目のサインインが必要で、その確認は少し待たな"
"いと届きません。自動では、1 回目に時間がかかったときに待ちます。"
#: src/lib/menu.sh:273
msgid ""
"Loads the list of apps again once a day, so new apps show up by themselves."
msgstr ""
"1 日 1 回アプリの一覧を読み込み直すので、新しいアプリが自動で現れます。"
#: src/lib/menu.sh:275 src/lib/menu.sh:330
msgid "Needs a systemd user session."
msgstr "systemd のユーザーセッションが必要です。"
#: src/lib/menu.sh:322 src/lib/menu.sh:325
msgid "Could not save the setting."
msgstr "設定を保存できませんでした。"
#: src/lib/menu.sh:371 src/lib/menu.sh:609
msgid "Settings"
msgstr "設定"
#: src/lib/menu.sh:372
msgid "↑↓ select ←→ or Space: change q: back"
msgstr "↑↓ 選択 ←→ またはスペース: 変更 q: 戻る"
#: src/lib/menu.sh:373
msgid "ON"
msgstr "オン"
#: src/lib/menu.sh:374
msgid "OFF"
msgstr "オフ"
#: src/lib/menu.sh:456 src/lib/menu.sh:608
msgid "Workspaces"
msgstr "ワークスペース"
#. TRANSLATORS: keep the letters, they are the keys.
#: src/lib/menu.sh:458
msgid ""
"↑↓ select r: refresh s: sign in again d: remove a: add q: back"
msgstr "↑↓ 選択 r: 更新 s: 再サインイン d: 削除 a: 追加 q: 戻る"
#: src/lib/menu.sh:459
msgid "No workspace yet. Press a to add one."
msgstr "ワークスペースはまだありません。a で追加できます。"
#: src/lib/menu.sh:484
#, sh-printf-format
msgid "refreshed %s"
msgstr "%sに更新"
#: src/lib/menu.sh:512
#, sh-printf-format
msgid "Remove \"%s\" and its apps?"
msgstr "「%s」とそのアプリを削除しますか?"
#: src/lib/menu.sh:536 src/lib/menu.sh:607
msgid "Start an app"
msgstr "アプリを起動する"
#: src/lib/menu.sh:537
msgid "↑↓ select Enter: start q: back"
msgstr "↑↓ 選択 Enter: 起動 q: 戻る"
#: src/lib/menu.sh:538
msgid "No apps yet. Add a workspace first."
msgstr "アプリはまだありません。先にワークスペースを追加してください。"
#: src/lib/menu.sh:579
#, sh-printf-format
msgid "%s is starting. It opens in a window of its own."
msgstr "%s を起動しています。専用のウィンドウで開きます。"
#: src/lib/menu.sh:606
msgid "Add a workspace"
msgstr "ワークスペースを追加"
#: src/lib/menu.sh:610
msgid "Quit"
msgstr "終了"
#: src/lib/workspace.sh:163
msgid "The server did not accept the user name or password."
msgstr "サーバーがユーザー名またはパスワードを受け付けませんでした。"
#: src/lib/workspace.sh:164
#, sh-printf-format
msgid "Could not reach the server: %s"
msgstr "サーバーに接続できませんでした: %s"
#: src/lib/workspace.sh:165
#, sh-printf-format
msgid "There is no RD Web feed at %s."
msgstr "%s に RD Web のフィードはありません。"
#: src/lib/workspace.sh:166
#, sh-printf-format
msgid "Could not load the apps: %s"
msgstr "アプリを読み込めませんでした: %s"
#: src/lib/workspace.sh:282
#, sh-printf-format
msgid "Remote desktop of %s"
msgstr "%s のリモートデスクトップ"
#: src/lib/workspace.sh:284
#, sh-printf-format
msgid "%s from %s"
msgstr "%s(%s)"
#: src/lib/menu.sh
msgid "Apps"
msgstr "アプリ"
#: src/lib/menu.sh
msgid "Share a folder"
msgstr "フォルダーを共有"
#: src/lib/menu.sh
msgid "Open files with the apps"
msgstr "ファイルをアプリで開く"
#: src/lib/menu.sh
msgid "Size"
msgstr "サイズ"
#: src/lib/menu.sh
msgid "Sound"
msgstr "音声"
#: src/lib/menu.sh
msgid "Graphics"
msgstr "グラフィック"
#: src/lib/menu.sh
msgid "Signing in"
msgstr "サインイン"
#: src/lib/menu.sh
msgid "Wait for the second MFA prompt"
msgstr "2 回目の MFA 確認を待つ"
#: src/lib/menu.sh
msgid "Refresh the apps every day"
msgstr "アプリを毎日更新"
+547
View File
@@ -0,0 +1,547 @@
# Korean translation for rdfeed.
# Copyright (C) 2026 Felitendo
# This file is distributed under the same license as rdfeed.
#
msgid ""
msgstr ""
"Project-Id-Version: rdfeed 1.0.0\n"
"Report-Msgid-Bugs-To: https://github.com/LoonixTools/rdfeed/issues\n"
"POT-Creation-Date: 2026-10-02 10:29+0200\n"
"PO-Revision-Date: 2026-10-02 09:30+0200\n"
"Last-Translator: Felitendo\n"
"Language-Team: Korean\n"
"Language: ko\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: src/rdfeed:79 src/rdfeed:103
#, sh-printf-format
msgid "Could not write to %s."
msgstr "%s에 쓰지 못했습니다."
#: src/rdfeed:82
#, sh-printf-format
msgid "Signing in to %s…"
msgstr "%s에 로그인하는 중…"
#: src/rdfeed:100
msgid ""
"The password could not be saved in the keyring, so each app asks for it."
msgstr "비밀번호를 키링에 저장하지 못해 앱마다 비밀번호를 묻습니다."
#: src/rdfeed:111
#, sh-printf-format
msgid "1 app from %s is now in your app menu."
msgstr "%s의 앱 1개가 앱 메뉴에 추가되었습니다."
#: src/rdfeed:113
#, sh-printf-format
msgid "%d apps from %s are now in your app menu."
msgstr "앱 %d개(%s)가 앱 메뉴에 추가되었습니다."
#: src/rdfeed:129
msgid ""
"Your IT department has the address. A work e-mail address often works too."
msgstr ""
"주소는 IT 부서에서 알려 줍니다. 회사 이메일 주소로도 되는 경우가 많습니다."
#: src/rdfeed:130
msgid "Feed address, server or e-mail:"
msgstr "피드 주소, 서버 또는 이메일:"
#: src/rdfeed:137
#, sh-printf-format
msgid "No feed is registered for %s. Ask your IT department for the address."
msgstr "%s에 등록된 피드가 없습니다. IT 부서에 주소를 물어보세요."
#: src/rdfeed:142 src/rdfeed:156
msgid "User name (DOMAIN\\name or name@domain):"
msgstr "사용자 이름 (DOMAIN\\name 또는 name@domain):"
#: src/rdfeed:146 src/rdfeed:159
msgid "Password:"
msgstr "비밀번호:"
#: src/rdfeed:178 src/rdfeed:236
#, sh-printf-format
msgid "No workspace yet. Add one with `%s add`."
msgstr "아직 작업 공간이 없습니다. `%s add`로 추가하세요."
#: src/rdfeed:187 src/lib/launch.sh:220
#, sh-printf-format
msgid "No password saved for %s. Sign in again in rdfeed."
msgstr "%s의 비밀번호가 저장되어 있지 않습니다. rdfeed에서 다시 로그인하세요."
#: src/rdfeed:191 src/lib/launch.sh:149
#, sh-printf-format
msgid "Password for %s:"
msgstr "%s 비밀번호:"
#: src/rdfeed:195
#, sh-printf-format
msgid "Refreshing %s…"
msgstr "%s 새로 고치는 중…"
#: src/rdfeed:205
msgid "The password no longer works. Sign in again in rdfeed."
msgstr "비밀번호가 더 이상 맞지 않습니다. rdfeed에서 다시 로그인하세요."
#: src/rdfeed:214
#, sh-printf-format
msgid "%s: %d apps"
msgstr "%s: 앱 %d개"
#: src/rdfeed:222
#, sh-printf-format
msgid "Which workspace? See `%s list` for the names."
msgstr "어느 작업 공간인가요? 이름은 `%s list`에서 볼 수 있습니다."
#: src/rdfeed:229
#, sh-printf-format
msgid "%s and its apps are removed."
msgstr "%s와(과) 해당 앱을 삭제했습니다."
#: src/rdfeed:253
#, sh-printf-format
msgid "Which app? See `%s list`."
msgstr "어느 앱인가요? `%s list`를 보세요."
#: src/rdfeed:268 src/lib/launch.sh:199
#, sh-printf-format
msgid "There is no app %s. See `%s list`."
msgstr "%s 앱이 없습니다. `%s list`를 보세요."
#: src/rdfeed:270
#, sh-printf-format
msgid "Several workspaces have %s. Write it as WORKSPACE/APP, see `%s list`."
msgstr ""
"여러 작업 공간에 %s이(가) 있습니다. 작업공간/앱 형식으로 쓰세요. `%s list`를 "
"보세요."
#: src/rdfeed:278
msgid "Usage: rdfeed [command]"
msgstr "사용법: rdfeed [명령]"
#: src/rdfeed:280
msgid "Commands:"
msgstr "명령:"
#: src/rdfeed:281
msgid "Add the workspace of your company"
msgstr "회사의 작업 공간 추가"
#: src/rdfeed:282
msgid "Load the apps again"
msgstr "앱 다시 불러오기"
#: src/rdfeed:283
msgid "List the workspaces and their apps"
msgstr "작업 공간과 앱 목록 보기"
#: src/rdfeed:284
msgid "Start an app, or open a file with it"
msgstr "앱 시작 또는 파일 열기"
#: src/rdfeed:285
msgid "Remove a workspace and its apps"
msgstr "작업 공간과 앱 삭제"
#: src/rdfeed:286
msgid "Show this help"
msgstr "이 도움말 보기"
#: src/rdfeed:287
msgid "Show the version"
msgstr "버전 보기"
#: src/rdfeed:289
msgid "Without a command an interactive menu is shown."
msgstr "명령 없이 실행하면 대화형 메뉴가 나옵니다."
#: src/rdfeed:313
#, sh-printf-format
msgid "Unknown command: %s"
msgstr "알 수 없는 명령: %s"
#: src/lib/common.sh:174 src/lib/menu.sh:251
msgid "never"
msgstr "없음"
#: src/lib/common.sh:184
msgid "just now"
msgstr "방금"
#: src/lib/common.sh:186
msgid "1 minute ago"
msgstr "1분 전"
#: src/lib/common.sh:189
#, sh-printf-format
msgid "%d minutes ago"
msgstr "%d분 전"
#: src/lib/common.sh:191
msgid "1 hour ago"
msgstr "1시간 전"
#: src/lib/common.sh:194
#, sh-printf-format
msgid "%d hours ago"
msgstr "%d시간 전"
#: src/lib/common.sh:196
msgid "1 day ago"
msgstr "1일 전"
#: src/lib/common.sh:199
#, sh-printf-format
msgid "%d days ago"
msgstr "%d일 전"
#: src/lib/launch.sh:169
msgid ""
"Your password has expired. Change it on Windows or on the RD Web page, then "
"sign in again in rdfeed."
msgstr ""
"비밀번호가 만료되었습니다. Windows나 RD Web 페이지에서 바꾼 다음 rdfeed에서 "
"다시 로그인하세요."
#: src/lib/launch.sh:171
msgid "Your account is locked. Ask your IT department."
msgstr "계정이 잠겼습니다. IT 부서에 문의하세요."
#: src/lib/launch.sh:173
msgid "The server did not accept the password. Sign in again in rdfeed."
msgstr "서버가 비밀번호를 받아들이지 않았습니다. rdfeed에서 다시 로그인하세요."
#: src/lib/launch.sh:175
msgid "Could not reach the server. Check the internet connection."
msgstr "서버에 연결하지 못했습니다. 인터넷 연결을 확인하세요."
#: src/lib/launch.sh:178
#, sh-printf-format
msgid "FreeRDP stopped: %s"
msgstr "FreeRDP가 멈췄습니다: %s"
#: src/lib/launch.sh:205 src/lib/launch.sh:206
#, sh-printf-format
msgid "File not found: %s"
msgstr "파일을 찾을 수 없습니다: %s"
#: src/lib/launch.sh:212 src/lib/launch.sh:213
msgid ""
"FreeRDP 3 is missing. Install it to start the apps (the package is called "
"freerdp)."
msgstr ""
"FreeRDP 3이 없습니다. 앱을 시작하려면 설치하세요(패키지 이름은 freerdp)."
#: src/lib/launch.sh:277
#, sh-printf-format
msgid "Opening %s in %s"
msgstr "%s 파일을 %s(으)로 여는 중"
#: src/lib/launch.sh:278
#, sh-printf-format
msgid "If it does not open by itself, open it in the app from the drive %s."
msgstr "저절로 열리지 않으면 앱에서 %s 드라이브의 파일을 여세요."
#: src/lib/launch.sh:280
#, sh-printf-format
msgid "Starting %s"
msgstr "%s 시작하는 중"
#: src/lib/launch.sh:281
msgid ""
"If your company uses MFA, confirm the sign-in in your authenticator app."
msgstr "회사에서 MFA를 쓴다면 인증 앱에서 로그인을 승인하세요."
#: src/lib/launch.sh:290
msgid "A second sign-in is coming"
msgstr "두 번째 로그인이 옵니다"
#: src/lib/launch.sh:291
#, sh-printf-format
msgid ""
"Confirm the second prompt in your authenticator app too. It comes in about "
"%d seconds."
msgstr "인증 앱에서 두 번째 요청도 승인하세요. 약 %d초 뒤에 옵니다."
#: src/lib/launch.sh:301
#, sh-printf-format
msgid "%s could not start"
msgstr "%s을(를) 시작하지 못했습니다"
#: src/lib/launch.sh:303
#, sh-printf-format
msgid "Log: %s"
msgstr "로그: %s"
#. TRANSLATORS: the letter after "[" is the key for yes. y works too.
#: src/lib/menu.sh:93
msgid "[y/N]"
msgstr "[y/N]"
#: src/lib/menu.sh:163
msgid "1 app"
msgstr "앱 1개"
#: src/lib/menu.sh:165
#, sh-printf-format
msgid "%d apps"
msgstr "앱 %d개"
#: src/lib/menu.sh:190
msgid "No workspace yet. Add the one of your company with [1]."
msgstr "아직 작업 공간이 없습니다. [1]로 회사의 작업 공간을 추가하세요."
#: src/lib/menu.sh:233
msgid "off"
msgstr "꺼짐"
#: src/lib/menu.sh:234
msgid "home folder"
msgstr "홈 폴더"
#: src/lib/menu.sh:237
msgid "Documents"
msgstr "문서"
#: src/lib/menu.sh:238
msgid "Downloads"
msgstr "다운로드"
#: src/lib/menu.sh:243
msgid "not found"
msgstr "찾을 수 없음"
#: src/lib/menu.sh:246
#, sh-printf-format
msgid "automatic (%d %%)"
msgstr "자동 (%d%%)"
#: src/lib/menu.sh:248
msgid "classic"
msgstr "클래식"
#: src/lib/menu.sh:249
msgid "GFX"
msgstr "GFX"
#: src/lib/menu.sh:250
msgid "always"
msgstr "항상"
#: src/lib/menu.sh:252
msgid "automatic"
msgstr "자동"
#: src/lib/menu.sh:261
msgid ""
"The apps only see the files on the server. Pick a folder to open and save "
"files of this computer in them."
msgstr ""
"앱은 서버의 파일만 봅니다. 폴더를 고르면 이 컴퓨터의 파일을 열고 저장할 수 있"
"습니다."
#: src/lib/menu.sh:262
msgid ""
"The apps can open and save files in this folder. It shows up as a drive in "
"them."
msgstr ""
"앱이 이 폴더의 파일을 열고 저장할 수 있습니다. 앱에서는 드라이브로 보입니다."
#: src/lib/menu.sh:263
msgid ""
"Offers the apps under \"Open with\" for their file types, for example Word "
"for .docx. The folder of the file is shared with the app while it is open."
msgstr ""
"파일 형식에 맞게 \"연결 프로그램\"에 앱을 표시합니다(예: .docx는 Word). 파일"
"이 열려 있는 동안 해당 폴더가 공유됩니다."
#: src/lib/menu.sh:264
msgid ""
"How big the apps are drawn. Automatic follows the scale of your desktop, so "
"they are sharp and the right size."
msgstr ""
"앱을 그리는 크기입니다. 자동은 데스크톱 배율을 따르므로 선명하고 알맞은 크기"
"로 보입니다."
#: src/lib/menu.sh:265
msgid "Plays the sound of the apps here, for calls and videos."
msgstr "앱의 소리를 이 컴퓨터에서 재생합니다. 통화와 동영상에 필요합니다."
#: src/lib/menu.sh:266
msgid ""
"Paints every window right. But some servers end the session right after "
"signing in, then go back to GFX."
msgstr "모든 창을 올바르게 그립니다. 하지만 로그인 직후 세션을 끊는 서버도 있으니 그때는 GFX로 돌아가세요."
#: src/lib/menu.sh:267
msgid ""
"Works with every server. FreeRDP may not repaint a window after it was "
"minimized or resized."
msgstr "모든 서버에서 됩니다. 최소화하거나 크기를 바꾼 창을 FreeRDP가 다시 그리지 않을 수 있습니다."
#: src/lib/menu.sh:268
msgid ""
"Always waits before the second sign-in. For when the second prompt does not "
"come with automatic."
msgstr ""
"두 번째 로그인 전에 항상 기다립니다. 자동일 때 두 번째 요청이 오지 않으면 쓰"
"세요."
#: src/lib/menu.sh:269
msgid "Never waits. Right when your company has no MFA."
msgstr "기다리지 않습니다. 회사에서 MFA를 쓰지 않을 때 알맞습니다."
#: src/lib/menu.sh:270
msgid ""
"Many servers with MFA need a second sign-in, and its prompt only comes after "
"a short wait. Automatic waits when the first sign-in took a while."
msgstr ""
"MFA를 쓰는 많은 서버는 두 번째 로그인이 필요하고, 그 요청은 잠시 기다린 뒤에"
"야 옵니다. 자동은 첫 로그인이 오래 걸렸을 때 기다립니다."
#: src/lib/menu.sh:273
msgid ""
"Loads the list of apps again once a day, so new apps show up by themselves."
msgstr "하루에 한 번 앱 목록을 다시 불러오므로 새 앱이 저절로 나타납니다."
#: src/lib/menu.sh:275 src/lib/menu.sh:330
msgid "Needs a systemd user session."
msgstr "systemd 사용자 세션이 필요합니다."
#: src/lib/menu.sh:322 src/lib/menu.sh:325
msgid "Could not save the setting."
msgstr "설정을 저장하지 못했습니다."
#: src/lib/menu.sh:371 src/lib/menu.sh:609
msgid "Settings"
msgstr "설정"
#: src/lib/menu.sh:372
msgid "↑↓ select ←→ or Space: change q: back"
msgstr "↑↓ 선택 ←→ 또는 스페이스: 변경 q: 뒤로"
#: src/lib/menu.sh:373
msgid "ON"
msgstr "켜짐"
#: src/lib/menu.sh:374
msgid "OFF"
msgstr "꺼짐"
#: src/lib/menu.sh:456 src/lib/menu.sh:608
msgid "Workspaces"
msgstr "작업 공간"
#. TRANSLATORS: keep the letters, they are the keys.
#: src/lib/menu.sh:458
msgid ""
"↑↓ select r: refresh s: sign in again d: remove a: add q: back"
msgstr "↑↓ 선택 r: 새로 고침 s: 다시 로그인 d: 삭제 a: 추가 q: 뒤로"
#: src/lib/menu.sh:459
msgid "No workspace yet. Press a to add one."
msgstr "아직 작업 공간이 없습니다. a를 눌러 추가하세요."
#: src/lib/menu.sh:484
#, sh-printf-format
msgid "refreshed %s"
msgstr "%s 새로 고침"
#: src/lib/menu.sh:512
#, sh-printf-format
msgid "Remove \"%s\" and its apps?"
msgstr "\"%s\"와(과) 해당 앱을 삭제할까요?"
#: src/lib/menu.sh:536 src/lib/menu.sh:607
msgid "Start an app"
msgstr "앱 시작"
#: src/lib/menu.sh:537
msgid "↑↓ select Enter: start q: back"
msgstr "↑↓ 선택 Enter: 시작 q: 뒤로"
#: src/lib/menu.sh:538
msgid "No apps yet. Add a workspace first."
msgstr "아직 앱이 없습니다. 먼저 작업 공간을 추가하세요."
#: src/lib/menu.sh:579
#, sh-printf-format
msgid "%s is starting. It opens in a window of its own."
msgstr "%s을(를) 시작하는 중입니다. 별도 창으로 열립니다."
#: src/lib/menu.sh:606
msgid "Add a workspace"
msgstr "작업 공간 추가"
#: src/lib/menu.sh:610
msgid "Quit"
msgstr "끝내기"
#: src/lib/workspace.sh:163
msgid "The server did not accept the user name or password."
msgstr "서버가 사용자 이름이나 비밀번호를 받아들이지 않았습니다."
#: src/lib/workspace.sh:164
#, sh-printf-format
msgid "Could not reach the server: %s"
msgstr "서버에 연결하지 못했습니다: %s"
#: src/lib/workspace.sh:165
#, sh-printf-format
msgid "There is no RD Web feed at %s."
msgstr "%s에 RD Web 피드가 없습니다."
#: src/lib/workspace.sh:166
#, sh-printf-format
msgid "Could not load the apps: %s"
msgstr "앱을 불러오지 못했습니다: %s"
#: src/lib/workspace.sh:282
#, sh-printf-format
msgid "Remote desktop of %s"
msgstr "%s 원격 데스크톱"
#: src/lib/workspace.sh:284
#, sh-printf-format
msgid "%s from %s"
msgstr "%s (%s)"
#: src/lib/menu.sh
msgid "Apps"
msgstr "앱"
#: src/lib/menu.sh
msgid "Share a folder"
msgstr "폴더 공유"
#: src/lib/menu.sh
msgid "Open files with the apps"
msgstr "앱으로 파일 열기"
#: src/lib/menu.sh
msgid "Size"
msgstr "크기"
#: src/lib/menu.sh
msgid "Sound"
msgstr "소리"
#: src/lib/menu.sh
msgid "Graphics"
msgstr "그래픽"
#: src/lib/menu.sh
msgid "Signing in"
msgstr "로그인"
#: src/lib/menu.sh
msgid "Wait for the second MFA prompt"
msgstr "두 번째 MFA 요청 기다리기"
#: src/lib/menu.sh
msgid "Refresh the apps every day"
msgstr "매일 앱 새로 고침"
+562
View File
@@ -0,0 +1,562 @@
# Dutch translation for rdfeed.
# Copyright (C) 2026 Felitendo
# This file is distributed under the same license as rdfeed.
#
msgid ""
msgstr ""
"Project-Id-Version: rdfeed 1.0.0\n"
"Report-Msgid-Bugs-To: https://github.com/LoonixTools/rdfeed/issues\n"
"POT-Creation-Date: 2026-10-02 10:29+0200\n"
"PO-Revision-Date: 2026-10-02 09:30+0200\n"
"Last-Translator: Felitendo\n"
"Language-Team: Dutch\n"
"Language: nl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: src/rdfeed:79 src/rdfeed:103
#, sh-printf-format
msgid "Could not write to %s."
msgstr "Kon niet schrijven naar %s."
#: src/rdfeed:82
#, sh-printf-format
msgid "Signing in to %s…"
msgstr "Aanmelden bij %s…"
#: src/rdfeed:100
msgid ""
"The password could not be saved in the keyring, so each app asks for it."
msgstr ""
"Het wachtwoord kon niet in de sleutelbos worden opgeslagen, dus elke app "
"vraagt erom."
#: src/rdfeed:111
#, sh-printf-format
msgid "1 app from %s is now in your app menu."
msgstr "1 app van %s staat nu in je app-menu."
#: src/rdfeed:113
#, sh-printf-format
msgid "%d apps from %s are now in your app menu."
msgstr "%d apps van %s staan nu in je app-menu."
#: src/rdfeed:129
msgid ""
"Your IT department has the address. A work e-mail address often works too."
msgstr "Je IT-afdeling heeft het adres. Vaak werkt je werk-e-mailadres ook."
#: src/rdfeed:130
msgid "Feed address, server or e-mail:"
msgstr "Feedadres, server of e-mail:"
#: src/rdfeed:137
#, sh-printf-format
msgid "No feed is registered for %s. Ask your IT department for the address."
msgstr ""
"Er is geen feed geregistreerd voor %s. Vraag je IT-afdeling om het adres."
#: src/rdfeed:142 src/rdfeed:156
msgid "User name (DOMAIN\\name or name@domain):"
msgstr "Gebruikersnaam (DOMEIN\\naam of naam@domein):"
#: src/rdfeed:146 src/rdfeed:159
msgid "Password:"
msgstr "Wachtwoord:"
#: src/rdfeed:178 src/rdfeed:236
#, sh-printf-format
msgid "No workspace yet. Add one with `%s add`."
msgstr "Nog geen werkruimte. Voeg er een toe met `%s add`."
#: src/rdfeed:187 src/lib/launch.sh:220
#, sh-printf-format
msgid "No password saved for %s. Sign in again in rdfeed."
msgstr "Geen wachtwoord opgeslagen voor %s. Meld je opnieuw aan in rdfeed."
#: src/rdfeed:191 src/lib/launch.sh:149
#, sh-printf-format
msgid "Password for %s:"
msgstr "Wachtwoord voor %s:"
#: src/rdfeed:195
#, sh-printf-format
msgid "Refreshing %s…"
msgstr "%s vernieuwen…"
#: src/rdfeed:205
msgid "The password no longer works. Sign in again in rdfeed."
msgstr "Het wachtwoord werkt niet meer. Meld je opnieuw aan in rdfeed."
#: src/rdfeed:214
#, sh-printf-format
msgid "%s: %d apps"
msgstr "%s: %d apps"
#: src/rdfeed:222
#, sh-printf-format
msgid "Which workspace? See `%s list` for the names."
msgstr "Welke werkruimte? De namen staan in `%s list`."
#: src/rdfeed:229
#, sh-printf-format
msgid "%s and its apps are removed."
msgstr "%s en de apps ervan zijn verwijderd."
#: src/rdfeed:253
#, sh-printf-format
msgid "Which app? See `%s list`."
msgstr "Welke app? Zie `%s list`."
#: src/rdfeed:268 src/lib/launch.sh:199
#, sh-printf-format
msgid "There is no app %s. See `%s list`."
msgstr "Er is geen app %s. Zie `%s list`."
#: src/rdfeed:270
#, sh-printf-format
msgid "Several workspaces have %s. Write it as WORKSPACE/APP, see `%s list`."
msgstr ""
"Meerdere werkruimtes hebben %s. Schrijf het als WERKRUIMTE/APP, zie `%s "
"list`."
#: src/rdfeed:278
msgid "Usage: rdfeed [command]"
msgstr "Gebruik: rdfeed [opdracht]"
#: src/rdfeed:280
msgid "Commands:"
msgstr "Opdrachten:"
#: src/rdfeed:281
msgid "Add the workspace of your company"
msgstr "De werkruimte van je bedrijf toevoegen"
#: src/rdfeed:282
msgid "Load the apps again"
msgstr "De apps opnieuw laden"
#: src/rdfeed:283
msgid "List the workspaces and their apps"
msgstr "Werkruimtes en hun apps tonen"
#: src/rdfeed:284
msgid "Start an app, or open a file with it"
msgstr "Een app starten, of er een bestand mee openen"
#: src/rdfeed:285
msgid "Remove a workspace and its apps"
msgstr "Een werkruimte en de apps ervan verwijderen"
#: src/rdfeed:286
msgid "Show this help"
msgstr "Deze hulp tonen"
#: src/rdfeed:287
msgid "Show the version"
msgstr "De versie tonen"
#: src/rdfeed:289
msgid "Without a command an interactive menu is shown."
msgstr "Zonder opdracht verschijnt een interactief menu."
#: src/rdfeed:313
#, sh-printf-format
msgid "Unknown command: %s"
msgstr "Onbekende opdracht: %s"
#: src/lib/common.sh:174 src/lib/menu.sh:251
msgid "never"
msgstr "nooit"
#: src/lib/common.sh:184
msgid "just now"
msgstr "zojuist"
#: src/lib/common.sh:186
msgid "1 minute ago"
msgstr "1 minuut geleden"
#: src/lib/common.sh:189
#, sh-printf-format
msgid "%d minutes ago"
msgstr "%d minuten geleden"
#: src/lib/common.sh:191
msgid "1 hour ago"
msgstr "1 uur geleden"
#: src/lib/common.sh:194
#, sh-printf-format
msgid "%d hours ago"
msgstr "%d uur geleden"
#: src/lib/common.sh:196
msgid "1 day ago"
msgstr "1 dag geleden"
#: src/lib/common.sh:199
#, sh-printf-format
msgid "%d days ago"
msgstr "%d dagen geleden"
#: src/lib/launch.sh:169
msgid ""
"Your password has expired. Change it on Windows or on the RD Web page, then "
"sign in again in rdfeed."
msgstr ""
"Je wachtwoord is verlopen. Wijzig het in Windows of op de RD Web-pagina en "
"meld je daarna opnieuw aan in rdfeed."
#: src/lib/launch.sh:171
msgid "Your account is locked. Ask your IT department."
msgstr "Je account is geblokkeerd. Vraag het je IT-afdeling."
#: src/lib/launch.sh:173
msgid "The server did not accept the password. Sign in again in rdfeed."
msgstr ""
"De server heeft het wachtwoord niet geaccepteerd. Meld je opnieuw aan in "
"rdfeed."
#: src/lib/launch.sh:175
msgid "Could not reach the server. Check the internet connection."
msgstr "Kon de server niet bereiken. Controleer de internetverbinding."
#: src/lib/launch.sh:178
#, sh-printf-format
msgid "FreeRDP stopped: %s"
msgstr "FreeRDP is gestopt: %s"
#: src/lib/launch.sh:205 src/lib/launch.sh:206
#, sh-printf-format
msgid "File not found: %s"
msgstr "Bestand niet gevonden: %s"
#: src/lib/launch.sh:212 src/lib/launch.sh:213
msgid ""
"FreeRDP 3 is missing. Install it to start the apps (the package is called "
"freerdp)."
msgstr ""
"FreeRDP 3 ontbreekt. Installeer het om de apps te starten (het pakket heet "
"freerdp)."
#: src/lib/launch.sh:277
#, sh-printf-format
msgid "Opening %s in %s"
msgstr "%s wordt geopend in %s"
#: src/lib/launch.sh:278
#, sh-printf-format
msgid "If it does not open by itself, open it in the app from the drive %s."
msgstr ""
"Gaat het niet vanzelf open, open het dan in de app vanaf het station %s."
#: src/lib/launch.sh:280
#, sh-printf-format
msgid "Starting %s"
msgstr "%s wordt gestart"
#: src/lib/launch.sh:281
msgid ""
"If your company uses MFA, confirm the sign-in in your authenticator app."
msgstr ""
"Gebruikt je bedrijf MFA, bevestig de aanmelding dan in je authenticator-app."
#: src/lib/launch.sh:290
msgid "A second sign-in is coming"
msgstr "Er komt een tweede aanmelding"
#: src/lib/launch.sh:291
#, sh-printf-format
msgid ""
"Confirm the second prompt in your authenticator app too. It comes in about "
"%d seconds."
msgstr ""
"Bevestig ook het tweede verzoek in je authenticator-app. Het komt over "
"ongeveer %d seconden."
#: src/lib/launch.sh:301
#, sh-printf-format
msgid "%s could not start"
msgstr "%s kon niet starten"
#: src/lib/launch.sh:303
#, sh-printf-format
msgid "Log: %s"
msgstr "Logboek: %s"
#. TRANSLATORS: the letter after "[" is the key for yes. y works too.
#: src/lib/menu.sh:93
msgid "[y/N]"
msgstr "[j/N]"
#: src/lib/menu.sh:163
msgid "1 app"
msgstr "1 app"
#: src/lib/menu.sh:165
#, sh-printf-format
msgid "%d apps"
msgstr "%d apps"
#: src/lib/menu.sh:190
msgid "No workspace yet. Add the one of your company with [1]."
msgstr "Nog geen werkruimte. Voeg die van je bedrijf toe met [1]."
#: src/lib/menu.sh:233
msgid "off"
msgstr "uit"
#: src/lib/menu.sh:234
msgid "home folder"
msgstr "thuismap"
#: src/lib/menu.sh:237
msgid "Documents"
msgstr "Documenten"
#: src/lib/menu.sh:238
msgid "Downloads"
msgstr "Downloads"
#: src/lib/menu.sh:243
msgid "not found"
msgstr "niet gevonden"
#: src/lib/menu.sh:246
#, sh-printf-format
msgid "automatic (%d %%)"
msgstr "automatisch (%d %%)"
#: src/lib/menu.sh:248
msgid "classic"
msgstr "klassiek"
#: src/lib/menu.sh:249
msgid "GFX"
msgstr "GFX"
#: src/lib/menu.sh:250
msgid "always"
msgstr "altijd"
#: src/lib/menu.sh:252
msgid "automatic"
msgstr "automatisch"
#: src/lib/menu.sh:261
msgid ""
"The apps only see the files on the server. Pick a folder to open and save "
"files of this computer in them."
msgstr ""
"De apps zien alleen de bestanden op de server. Kies een map, dan kunnen ze "
"bestanden van deze computer openen en opslaan."
#: src/lib/menu.sh:262
msgid ""
"The apps can open and save files in this folder. It shows up as a drive in "
"them."
msgstr ""
"De apps kunnen in deze map bestanden openen en opslaan. Hij verschijnt "
"daarin als station."
#: src/lib/menu.sh:263
msgid ""
"Offers the apps under \"Open with\" for their file types, for example Word "
"for .docx. The folder of the file is shared with the app while it is open."
msgstr ""
"Biedt de apps aan onder \"Openen met\" voor hun bestandstypen, bijvoorbeeld "
"Word voor .docx. De map van het bestand wordt gedeeld zolang het open is."
#: src/lib/menu.sh:264
msgid ""
"How big the apps are drawn. Automatic follows the scale of your desktop, so "
"they are sharp and the right size."
msgstr ""
"Hoe groot de apps worden getekend. Automatisch volgt de schaal van je "
"bureaublad, zo zijn ze scherp en goed van formaat."
#: src/lib/menu.sh:265
msgid "Plays the sound of the apps here, for calls and videos."
msgstr "Speelt het geluid van de apps hier af, voor gesprekken en video's."
#: src/lib/menu.sh:266
msgid ""
"Paints every window right. But some servers end the session right after "
"signing in, then go back to GFX."
msgstr "Tekent elk venster goed. Maar sommige servers beëindigen de sessie direct na het aanmelden; ga dan terug naar GFX."
#: src/lib/menu.sh:267
msgid ""
"Works with every server. FreeRDP may not repaint a window after it was "
"minimized or resized."
msgstr "Werkt met elke server. FreeRDP tekent een venster na minimaliseren of van formaat wijzigen misschien niet opnieuw."
#: src/lib/menu.sh:268
msgid ""
"Always waits before the second sign-in. For when the second prompt does not "
"come with automatic."
msgstr ""
"Wacht altijd voor de tweede aanmelding. Voor als het tweede verzoek met "
"automatisch niet komt."
#: src/lib/menu.sh:269
msgid "Never waits. Right when your company has no MFA."
msgstr "Wacht nooit. Goed als je bedrijf geen MFA gebruikt."
#: src/lib/menu.sh:270
msgid ""
"Many servers with MFA need a second sign-in, and its prompt only comes after "
"a short wait. Automatic waits when the first sign-in took a while."
msgstr ""
"Veel servers met MFA vragen een tweede aanmelding, en het verzoek daarvoor "
"komt pas na even wachten. Automatisch wacht als de eerste aanmelding even "
"duurde."
#: src/lib/menu.sh:273
msgid ""
"Loads the list of apps again once a day, so new apps show up by themselves."
msgstr ""
"Laadt de lijst met apps één keer per dag opnieuw, zo verschijnen nieuwe apps "
"vanzelf."
#: src/lib/menu.sh:275 src/lib/menu.sh:330
msgid "Needs a systemd user session."
msgstr "Vereist een systemd-gebruikerssessie."
#: src/lib/menu.sh:322 src/lib/menu.sh:325
msgid "Could not save the setting."
msgstr "Kon de instelling niet opslaan."
#: src/lib/menu.sh:371 src/lib/menu.sh:609
msgid "Settings"
msgstr "Instellingen"
#: src/lib/menu.sh:372
msgid "↑↓ select ←→ or Space: change q: back"
msgstr "↑↓ kiezen ←→ of Spatie: wijzigen q: terug"
#: src/lib/menu.sh:373
msgid "ON"
msgstr "AAN"
#: src/lib/menu.sh:374
msgid "OFF"
msgstr "UIT"
#: src/lib/menu.sh:456 src/lib/menu.sh:608
msgid "Workspaces"
msgstr "Werkruimtes"
#. TRANSLATORS: keep the letters, they are the keys.
#: src/lib/menu.sh:458
msgid ""
"↑↓ select r: refresh s: sign in again d: remove a: add q: back"
msgstr ""
"↑↓ kiezen r: vernieuwen s: opnieuw aanmelden d: verwijderen a: "
"toevoegen q: terug"
#: src/lib/menu.sh:459
msgid "No workspace yet. Press a to add one."
msgstr "Nog geen werkruimte. Druk op a om er een toe te voegen."
#: src/lib/menu.sh:484
#, sh-printf-format
msgid "refreshed %s"
msgstr "vernieuwd %s"
#: src/lib/menu.sh:512
#, sh-printf-format
msgid "Remove \"%s\" and its apps?"
msgstr "\"%s\" en de apps ervan verwijderen?"
#: src/lib/menu.sh:536 src/lib/menu.sh:607
msgid "Start an app"
msgstr "Een app starten"
#: src/lib/menu.sh:537
msgid "↑↓ select Enter: start q: back"
msgstr "↑↓ kiezen Enter: starten q: terug"
#: src/lib/menu.sh:538
msgid "No apps yet. Add a workspace first."
msgstr "Nog geen apps. Voeg eerst een werkruimte toe."
#: src/lib/menu.sh:579
#, sh-printf-format
msgid "%s is starting. It opens in a window of its own."
msgstr "%s wordt gestart. Het opent in een eigen venster."
#: src/lib/menu.sh:606
msgid "Add a workspace"
msgstr "Werkruimte toevoegen"
#: src/lib/menu.sh:610
msgid "Quit"
msgstr "Afsluiten"
#: src/lib/workspace.sh:163
msgid "The server did not accept the user name or password."
msgstr "De server heeft de gebruikersnaam of het wachtwoord niet geaccepteerd."
#: src/lib/workspace.sh:164
#, sh-printf-format
msgid "Could not reach the server: %s"
msgstr "Kon de server niet bereiken: %s"
#: src/lib/workspace.sh:165
#, sh-printf-format
msgid "There is no RD Web feed at %s."
msgstr "Er is geen RD Web-feed op %s."
#: src/lib/workspace.sh:166
#, sh-printf-format
msgid "Could not load the apps: %s"
msgstr "Kon de apps niet laden: %s"
#: src/lib/workspace.sh:282
#, sh-printf-format
msgid "Remote desktop of %s"
msgstr "Extern bureaublad van %s"
#: src/lib/workspace.sh:284
#, sh-printf-format
msgid "%s from %s"
msgstr "%s van %s"
#: src/lib/menu.sh
msgid "Apps"
msgstr "Apps"
#: src/lib/menu.sh
msgid "Share a folder"
msgstr "Map delen"
#: src/lib/menu.sh
msgid "Open files with the apps"
msgstr "Bestanden openen met de apps"
#: src/lib/menu.sh
msgid "Size"
msgstr "Grootte"
#: src/lib/menu.sh
msgid "Sound"
msgstr "Geluid"
#: src/lib/menu.sh
msgid "Graphics"
msgstr "Weergave"
#: src/lib/menu.sh
msgid "Signing in"
msgstr "Aanmelden"
#: src/lib/menu.sh
msgid "Wait for the second MFA prompt"
msgstr "Wachten op tweede MFA-verzoek"
#: src/lib/menu.sh
msgid "Refresh the apps every day"
msgstr "Apps elke dag vernieuwen"
+555
View File
@@ -0,0 +1,555 @@
# Polish translation for rdfeed.
# Copyright (C) 2026 Felitendo
# This file is distributed under the same license as rdfeed.
#
msgid ""
msgstr ""
"Project-Id-Version: rdfeed 1.0.0\n"
"Report-Msgid-Bugs-To: https://github.com/LoonixTools/rdfeed/issues\n"
"POT-Creation-Date: 2026-10-02 10:29+0200\n"
"PO-Revision-Date: 2026-10-02 09:30+0200\n"
"Last-Translator: Felitendo\n"
"Language-Team: Polish\n"
"Language: pl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: src/rdfeed:79 src/rdfeed:103
#, sh-printf-format
msgid "Could not write to %s."
msgstr "Nie udało się zapisać w %s."
#: src/rdfeed:82
#, sh-printf-format
msgid "Signing in to %s…"
msgstr "Logowanie do %s…"
#: src/rdfeed:100
msgid ""
"The password could not be saved in the keyring, so each app asks for it."
msgstr ""
"Nie udało się zapisać hasła w pęku kluczy, więc każda aplikacja o nie zapyta."
#: src/rdfeed:111
#, sh-printf-format
msgid "1 app from %s is now in your app menu."
msgstr "1 aplikacja z %s jest teraz w menu aplikacji."
#: src/rdfeed:113
#, sh-printf-format
msgid "%d apps from %s are now in your app menu."
msgstr "Dodano aplikacje (%d) z %s do menu aplikacji."
#: src/rdfeed:129
msgid ""
"Your IT department has the address. A work e-mail address often works too."
msgstr "Adres ma twój dział IT. Często działa też służbowy adres e-mail."
#: src/rdfeed:130
msgid "Feed address, server or e-mail:"
msgstr "Adres kanału, serwer lub e-mail:"
#: src/rdfeed:137
#, sh-printf-format
msgid "No feed is registered for %s. Ask your IT department for the address."
msgstr "Dla %s nie zarejestrowano kanału. Zapytaj dział IT o adres."
#: src/rdfeed:142 src/rdfeed:156
msgid "User name (DOMAIN\\name or name@domain):"
msgstr "Nazwa użytkownika (DOMENA\\nazwa lub nazwa@domena):"
#: src/rdfeed:146 src/rdfeed:159
msgid "Password:"
msgstr "Hasło:"
#: src/rdfeed:178 src/rdfeed:236
#, sh-printf-format
msgid "No workspace yet. Add one with `%s add`."
msgstr "Nie ma jeszcze obszaru roboczego. Dodaj go poleceniem `%s add`."
#: src/rdfeed:187 src/lib/launch.sh:220
#, sh-printf-format
msgid "No password saved for %s. Sign in again in rdfeed."
msgstr "Brak zapisanego hasła dla %s. Zaloguj się ponownie w rdfeed."
#: src/rdfeed:191 src/lib/launch.sh:149
#, sh-printf-format
msgid "Password for %s:"
msgstr "Hasło dla %s:"
#: src/rdfeed:195
#, sh-printf-format
msgid "Refreshing %s…"
msgstr "Odświeżanie %s…"
#: src/rdfeed:205
msgid "The password no longer works. Sign in again in rdfeed."
msgstr "Hasło już nie działa. Zaloguj się ponownie w rdfeed."
#: src/rdfeed:214
#, sh-printf-format
msgid "%s: %d apps"
msgstr "%s: aplikacje: %d"
#: src/rdfeed:222
#, sh-printf-format
msgid "Which workspace? See `%s list` for the names."
msgstr "Który obszar roboczy? Nazwy pokazuje `%s list`."
#: src/rdfeed:229
#, sh-printf-format
msgid "%s and its apps are removed."
msgstr "Usunięto %s i jego aplikacje."
#: src/rdfeed:253
#, sh-printf-format
msgid "Which app? See `%s list`."
msgstr "Która aplikacja? Zobacz `%s list`."
#: src/rdfeed:268 src/lib/launch.sh:199
#, sh-printf-format
msgid "There is no app %s. See `%s list`."
msgstr "Nie ma aplikacji %s. Zobacz `%s list`."
#: src/rdfeed:270
#, sh-printf-format
msgid "Several workspaces have %s. Write it as WORKSPACE/APP, see `%s list`."
msgstr ""
"Kilka obszarów roboczych ma %s. Wpisz to jako OBSZAR/APLIKACJA, zobacz `%s "
"list`."
#: src/rdfeed:278
msgid "Usage: rdfeed [command]"
msgstr "Użycie: rdfeed [polecenie]"
#: src/rdfeed:280
msgid "Commands:"
msgstr "Polecenia:"
#: src/rdfeed:281
msgid "Add the workspace of your company"
msgstr "Dodaj obszar roboczy swojej firmy"
#: src/rdfeed:282
msgid "Load the apps again"
msgstr "Wczytaj aplikacje ponownie"
#: src/rdfeed:283
msgid "List the workspaces and their apps"
msgstr "Wyświetl obszary robocze i ich aplikacje"
#: src/rdfeed:284
msgid "Start an app, or open a file with it"
msgstr "Uruchom aplikację lub otwórz nią plik"
#: src/rdfeed:285
msgid "Remove a workspace and its apps"
msgstr "Usuń obszar roboczy i jego aplikacje"
#: src/rdfeed:286
msgid "Show this help"
msgstr "Pokaż tę pomoc"
#: src/rdfeed:287
msgid "Show the version"
msgstr "Pokaż wersję"
#: src/rdfeed:289
msgid "Without a command an interactive menu is shown."
msgstr "Bez polecenia wyświetla się interaktywne menu."
#: src/rdfeed:313
#, sh-printf-format
msgid "Unknown command: %s"
msgstr "Nieznane polecenie: %s"
#: src/lib/common.sh:174 src/lib/menu.sh:251
msgid "never"
msgstr "nigdy"
#: src/lib/common.sh:184
msgid "just now"
msgstr "przed chwilą"
#: src/lib/common.sh:186
msgid "1 minute ago"
msgstr "1 minutę temu"
#: src/lib/common.sh:189
#, sh-printf-format
msgid "%d minutes ago"
msgstr "%d min temu"
#: src/lib/common.sh:191
msgid "1 hour ago"
msgstr "1 godzinę temu"
#: src/lib/common.sh:194
#, sh-printf-format
msgid "%d hours ago"
msgstr "%d godz. temu"
#: src/lib/common.sh:196
msgid "1 day ago"
msgstr "1 dzień temu"
#: src/lib/common.sh:199
#, sh-printf-format
msgid "%d days ago"
msgstr "%d dni temu"
#: src/lib/launch.sh:169
msgid ""
"Your password has expired. Change it on Windows or on the RD Web page, then "
"sign in again in rdfeed."
msgstr ""
"Hasło wygasło. Zmień je w Windows lub na stronie RD Web, a potem zaloguj się "
"ponownie w rdfeed."
#: src/lib/launch.sh:171
msgid "Your account is locked. Ask your IT department."
msgstr "Konto jest zablokowane. Zapytaj dział IT."
#: src/lib/launch.sh:173
msgid "The server did not accept the password. Sign in again in rdfeed."
msgstr "Serwer nie przyjął hasła. Zaloguj się ponownie w rdfeed."
#: src/lib/launch.sh:175
msgid "Could not reach the server. Check the internet connection."
msgstr "Nie udało się połączyć z serwerem. Sprawdź połączenie z internetem."
#: src/lib/launch.sh:178
#, sh-printf-format
msgid "FreeRDP stopped: %s"
msgstr "FreeRDP zatrzymał się: %s"
#: src/lib/launch.sh:205 src/lib/launch.sh:206
#, sh-printf-format
msgid "File not found: %s"
msgstr "Nie znaleziono pliku: %s"
#: src/lib/launch.sh:212 src/lib/launch.sh:213
msgid ""
"FreeRDP 3 is missing. Install it to start the apps (the package is called "
"freerdp)."
msgstr ""
"Brakuje FreeRDP 3. Zainstaluj go, aby uruchamiać aplikacje (pakiet nazywa "
"się freerdp)."
#: src/lib/launch.sh:277
#, sh-printf-format
msgid "Opening %s in %s"
msgstr "Otwieranie %s w %s"
#: src/lib/launch.sh:278
#, sh-printf-format
msgid "If it does not open by itself, open it in the app from the drive %s."
msgstr "Jeśli nie otworzy się sam, otwórz go w aplikacji z dysku %s."
#: src/lib/launch.sh:280
#, sh-printf-format
msgid "Starting %s"
msgstr "Uruchamianie: %s"
#: src/lib/launch.sh:281
msgid ""
"If your company uses MFA, confirm the sign-in in your authenticator app."
msgstr ""
"Jeśli firma używa MFA, potwierdź logowanie w aplikacji uwierzytelniającej."
#: src/lib/launch.sh:290
msgid "A second sign-in is coming"
msgstr "Nadchodzi drugie logowanie"
#: src/lib/launch.sh:291
#, sh-printf-format
msgid ""
"Confirm the second prompt in your authenticator app too. It comes in about "
"%d seconds."
msgstr ""
"Potwierdź też drugie żądanie w aplikacji uwierzytelniającej. Przyjdzie za "
"około %d s."
#: src/lib/launch.sh:301
#, sh-printf-format
msgid "%s could not start"
msgstr "Nie udało się uruchomić: %s"
#: src/lib/launch.sh:303
#, sh-printf-format
msgid "Log: %s"
msgstr "Dziennik: %s"
#. TRANSLATORS: the letter after "[" is the key for yes. y works too.
#: src/lib/menu.sh:93
msgid "[y/N]"
msgstr "[t/N]"
#: src/lib/menu.sh:163
msgid "1 app"
msgstr "1 aplikacja"
#: src/lib/menu.sh:165
#, sh-printf-format
msgid "%d apps"
msgstr "aplikacje: %d"
#: src/lib/menu.sh:190
msgid "No workspace yet. Add the one of your company with [1]."
msgstr "Nie ma jeszcze obszaru roboczego. Dodaj obszar swojej firmy przez [1]."
#: src/lib/menu.sh:233
msgid "off"
msgstr "wył."
#: src/lib/menu.sh:234
msgid "home folder"
msgstr "folder domowy"
#: src/lib/menu.sh:237
msgid "Documents"
msgstr "Dokumenty"
#: src/lib/menu.sh:238
msgid "Downloads"
msgstr "Pobrane"
#: src/lib/menu.sh:243
msgid "not found"
msgstr "nie znaleziono"
#: src/lib/menu.sh:246
#, sh-printf-format
msgid "automatic (%d %%)"
msgstr "automatyczny (%d %%)"
#: src/lib/menu.sh:248
msgid "classic"
msgstr "klasyczny"
#: src/lib/menu.sh:249
msgid "GFX"
msgstr "GFX"
#: src/lib/menu.sh:250
msgid "always"
msgstr "zawsze"
#: src/lib/menu.sh:252
msgid "automatic"
msgstr "automatycznie"
#: src/lib/menu.sh:261
msgid ""
"The apps only see the files on the server. Pick a folder to open and save "
"files of this computer in them."
msgstr ""
"Aplikacje widzą tylko pliki na serwerze. Wybierz folder, aby mogły otwierać "
"i zapisywać pliki z tego komputera."
#: src/lib/menu.sh:262
msgid ""
"The apps can open and save files in this folder. It shows up as a drive in "
"them."
msgstr ""
"Aplikacje mogą otwierać i zapisywać pliki w tym folderze. Widzą go jako dysk."
#: src/lib/menu.sh:263
msgid ""
"Offers the apps under \"Open with\" for their file types, for example Word "
"for .docx. The folder of the file is shared with the app while it is open."
msgstr ""
"Pokazuje aplikacje w „Otwórz za pomocą” dla ich typów plików, na przykład "
"Word dla .docx. Folder pliku jest udostępniany, dopóki plik jest otwarty."
#: src/lib/menu.sh:264
msgid ""
"How big the apps are drawn. Automatic follows the scale of your desktop, so "
"they are sharp and the right size."
msgstr ""
"Jak duże są rysowane aplikacje. Automatyczny podąża za skalą pulpitu, więc "
"są ostre i mają właściwy rozmiar."
#: src/lib/menu.sh:265
msgid "Plays the sound of the apps here, for calls and videos."
msgstr "Odtwarza tutaj dźwięk aplikacji, do rozmów i filmów."
#: src/lib/menu.sh:266
msgid ""
"Paints every window right. But some servers end the session right after "
"signing in, then go back to GFX."
msgstr "Rysuje każde okno poprawnie. Ale niektóre serwery kończą sesję zaraz po zalogowaniu, wtedy wróć do GFX."
#: src/lib/menu.sh:267
msgid ""
"Works with every server. FreeRDP may not repaint a window after it was "
"minimized or resized."
msgstr "Działa z każdym serwerem. FreeRDP może nie odrysować okna po zminimalizowaniu lub zmianie rozmiaru."
#: src/lib/menu.sh:268
msgid ""
"Always waits before the second sign-in. For when the second prompt does not "
"come with automatic."
msgstr ""
"Zawsze czeka przed drugim logowaniem. Gdy przy ustawieniu automatycznym "
"drugie żądanie nie przychodzi."
#: src/lib/menu.sh:269
msgid "Never waits. Right when your company has no MFA."
msgstr "Nigdy nie czeka. Właściwe, gdy firma nie używa MFA."
#: src/lib/menu.sh:270
msgid ""
"Many servers with MFA need a second sign-in, and its prompt only comes after "
"a short wait. Automatic waits when the first sign-in took a while."
msgstr ""
"Wiele serwerów z MFA wymaga drugiego logowania, a jego żądanie przychodzi "
"dopiero po chwili. Automatycznie czeka, gdy pierwsze logowanie trochę trwało."
#: src/lib/menu.sh:273
msgid ""
"Loads the list of apps again once a day, so new apps show up by themselves."
msgstr ""
"Wczytuje listę aplikacji raz dziennie, więc nowe aplikacje pojawiają się "
"same."
#: src/lib/menu.sh:275 src/lib/menu.sh:330
msgid "Needs a systemd user session."
msgstr "Wymaga sesji użytkownika systemd."
#: src/lib/menu.sh:322 src/lib/menu.sh:325
msgid "Could not save the setting."
msgstr "Nie udało się zapisać ustawienia."
#: src/lib/menu.sh:371 src/lib/menu.sh:609
msgid "Settings"
msgstr "Ustawienia"
#: src/lib/menu.sh:372
msgid "↑↓ select ←→ or Space: change q: back"
msgstr "↑↓ wybierz ←→ lub Spacja: zmień q: wstecz"
#: src/lib/menu.sh:373
msgid "ON"
msgstr "WŁ."
#: src/lib/menu.sh:374
msgid "OFF"
msgstr "WYŁ."
#: src/lib/menu.sh:456 src/lib/menu.sh:608
msgid "Workspaces"
msgstr "Obszary robocze"
#. TRANSLATORS: keep the letters, they are the keys.
#: src/lib/menu.sh:458
msgid ""
"↑↓ select r: refresh s: sign in again d: remove a: add q: back"
msgstr ""
"↑↓ wybierz r: odśwież s: zaloguj ponownie d: usuń a: dodaj q: "
"wstecz"
#: src/lib/menu.sh:459
msgid "No workspace yet. Press a to add one."
msgstr "Nie ma jeszcze obszaru roboczego. Naciśnij a, aby dodać."
#: src/lib/menu.sh:484
#, sh-printf-format
msgid "refreshed %s"
msgstr "odświeżono %s"
#: src/lib/menu.sh:512
#, sh-printf-format
msgid "Remove \"%s\" and its apps?"
msgstr "Usunąć „%s” i jego aplikacje?"
#: src/lib/menu.sh:536 src/lib/menu.sh:607
msgid "Start an app"
msgstr "Uruchom aplikację"
#: src/lib/menu.sh:537
msgid "↑↓ select Enter: start q: back"
msgstr "↑↓ wybierz Enter: uruchom q: wstecz"
#: src/lib/menu.sh:538
msgid "No apps yet. Add a workspace first."
msgstr "Nie ma jeszcze aplikacji. Najpierw dodaj obszar roboczy."
#: src/lib/menu.sh:579
#, sh-printf-format
msgid "%s is starting. It opens in a window of its own."
msgstr "Uruchamianie: %s. Otworzy się we własnym oknie."
#: src/lib/menu.sh:606
msgid "Add a workspace"
msgstr "Dodaj obszar roboczy"
#: src/lib/menu.sh:610
msgid "Quit"
msgstr "Zakończ"
#: src/lib/workspace.sh:163
msgid "The server did not accept the user name or password."
msgstr "Serwer nie przyjął nazwy użytkownika lub hasła."
#: src/lib/workspace.sh:164
#, sh-printf-format
msgid "Could not reach the server: %s"
msgstr "Nie udało się połączyć z serwerem: %s"
#: src/lib/workspace.sh:165
#, sh-printf-format
msgid "There is no RD Web feed at %s."
msgstr "Pod adresem %s nie ma kanału RD Web."
#: src/lib/workspace.sh:166
#, sh-printf-format
msgid "Could not load the apps: %s"
msgstr "Nie udało się wczytać aplikacji: %s"
#: src/lib/workspace.sh:282
#, sh-printf-format
msgid "Remote desktop of %s"
msgstr "Pulpit zdalny %s"
#: src/lib/workspace.sh:284
#, sh-printf-format
msgid "%s from %s"
msgstr "%s z %s"
#: src/lib/menu.sh
msgid "Apps"
msgstr "Aplikacje"
#: src/lib/menu.sh
msgid "Share a folder"
msgstr "Udostępnij folder"
#: src/lib/menu.sh
msgid "Open files with the apps"
msgstr "Otwieraj pliki w aplikacjach"
#: src/lib/menu.sh
msgid "Size"
msgstr "Rozmiar"
#: src/lib/menu.sh
msgid "Sound"
msgstr "Dźwięk"
#: src/lib/menu.sh
msgid "Graphics"
msgstr "Grafika"
#: src/lib/menu.sh
msgid "Signing in"
msgstr "Logowanie"
#: src/lib/menu.sh
msgid "Wait for the second MFA prompt"
msgstr "Czekaj na drugie żądanie MFA"
#: src/lib/menu.sh
msgid "Refresh the apps every day"
msgstr "Odświeżaj aplikacje codziennie"
+561
View File
@@ -0,0 +1,561 @@
# Brazilian Portuguese translation for rdfeed.
# Copyright (C) 2026 Felitendo
# This file is distributed under the same license as rdfeed.
#
msgid ""
msgstr ""
"Project-Id-Version: rdfeed 1.0.0\n"
"Report-Msgid-Bugs-To: https://github.com/LoonixTools/rdfeed/issues\n"
"POT-Creation-Date: 2026-10-02 10:29+0200\n"
"PO-Revision-Date: 2026-10-02 09:30+0200\n"
"Last-Translator: Felitendo\n"
"Language-Team: Brazilian Portuguese\n"
"Language: pt_BR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: src/rdfeed:79 src/rdfeed:103
#, sh-printf-format
msgid "Could not write to %s."
msgstr "Não foi possível gravar em %s."
#: src/rdfeed:82
#, sh-printf-format
msgid "Signing in to %s…"
msgstr "Entrando em %s…"
#: src/rdfeed:100
msgid ""
"The password could not be saved in the keyring, so each app asks for it."
msgstr ""
"Não foi possível salvar a senha no chaveiro, então cada app vai pedi-la."
#: src/rdfeed:111
#, sh-printf-format
msgid "1 app from %s is now in your app menu."
msgstr "1 app de %s agora está no seu menu de aplicativos."
#: src/rdfeed:113
#, sh-printf-format
msgid "%d apps from %s are now in your app menu."
msgstr "%d apps de %s agora estão no seu menu de aplicativos."
#: src/rdfeed:129
msgid ""
"Your IT department has the address. A work e-mail address often works too."
msgstr ""
"O seu departamento de TI tem o endereço. Muitas vezes o e-mail do trabalho "
"também funciona."
#: src/rdfeed:130
msgid "Feed address, server or e-mail:"
msgstr "Endereço do feed, servidor ou e-mail:"
#: src/rdfeed:137
#, sh-printf-format
msgid "No feed is registered for %s. Ask your IT department for the address."
msgstr ""
"Nenhum feed está registrado para %s. Peça o endereço ao seu departamento de "
"TI."
#: src/rdfeed:142 src/rdfeed:156
msgid "User name (DOMAIN\\name or name@domain):"
msgstr "Nome de usuário (DOMÍNIO\\nome ou nome@domínio):"
#: src/rdfeed:146 src/rdfeed:159
msgid "Password:"
msgstr "Senha:"
#: src/rdfeed:178 src/rdfeed:236
#, sh-printf-format
msgid "No workspace yet. Add one with `%s add`."
msgstr "Ainda não há espaço de trabalho. Adicione um com `%s add`."
#: src/rdfeed:187 src/lib/launch.sh:220
#, sh-printf-format
msgid "No password saved for %s. Sign in again in rdfeed."
msgstr "Nenhuma senha salva para %s. Entre de novo no rdfeed."
#: src/rdfeed:191 src/lib/launch.sh:149
#, sh-printf-format
msgid "Password for %s:"
msgstr "Senha de %s:"
#: src/rdfeed:195
#, sh-printf-format
msgid "Refreshing %s…"
msgstr "Atualizando %s…"
#: src/rdfeed:205
msgid "The password no longer works. Sign in again in rdfeed."
msgstr "A senha não funciona mais. Entre de novo no rdfeed."
#: src/rdfeed:214
#, sh-printf-format
msgid "%s: %d apps"
msgstr "%s: %d apps"
#: src/rdfeed:222
#, sh-printf-format
msgid "Which workspace? See `%s list` for the names."
msgstr "Qual espaço de trabalho? Os nomes estão em `%s list`."
#: src/rdfeed:229
#, sh-printf-format
msgid "%s and its apps are removed."
msgstr "%s e os seus apps foram removidos."
#: src/rdfeed:253
#, sh-printf-format
msgid "Which app? See `%s list`."
msgstr "Qual app? Veja `%s list`."
#: src/rdfeed:268 src/lib/launch.sh:199
#, sh-printf-format
msgid "There is no app %s. See `%s list`."
msgstr "Não existe o app %s. Veja `%s list`."
#: src/rdfeed:270
#, sh-printf-format
msgid "Several workspaces have %s. Write it as WORKSPACE/APP, see `%s list`."
msgstr ""
"Vários espaços de trabalho têm %s. Escreva como ESPAÇO/APP, veja `%s list`."
#: src/rdfeed:278
msgid "Usage: rdfeed [command]"
msgstr "Uso: rdfeed [comando]"
#: src/rdfeed:280
msgid "Commands:"
msgstr "Comandos:"
#: src/rdfeed:281
msgid "Add the workspace of your company"
msgstr "Adicionar o espaço de trabalho da sua empresa"
#: src/rdfeed:282
msgid "Load the apps again"
msgstr "Carregar os apps de novo"
#: src/rdfeed:283
msgid "List the workspaces and their apps"
msgstr "Listar os espaços de trabalho e seus apps"
#: src/rdfeed:284
msgid "Start an app, or open a file with it"
msgstr "Abrir um app, ou um arquivo com ele"
#: src/rdfeed:285
msgid "Remove a workspace and its apps"
msgstr "Remover um espaço de trabalho e seus apps"
#: src/rdfeed:286
msgid "Show this help"
msgstr "Mostrar esta ajuda"
#: src/rdfeed:287
msgid "Show the version"
msgstr "Mostrar a versão"
#: src/rdfeed:289
msgid "Without a command an interactive menu is shown."
msgstr "Sem comando, um menu interativo é exibido."
#: src/rdfeed:313
#, sh-printf-format
msgid "Unknown command: %s"
msgstr "Comando desconhecido: %s"
#: src/lib/common.sh:174 src/lib/menu.sh:251
msgid "never"
msgstr "nunca"
#: src/lib/common.sh:184
msgid "just now"
msgstr "agora mesmo"
#: src/lib/common.sh:186
msgid "1 minute ago"
msgstr "há 1 minuto"
#: src/lib/common.sh:189
#, sh-printf-format
msgid "%d minutes ago"
msgstr "há %d minutos"
#: src/lib/common.sh:191
msgid "1 hour ago"
msgstr "há 1 hora"
#: src/lib/common.sh:194
#, sh-printf-format
msgid "%d hours ago"
msgstr "há %d horas"
#: src/lib/common.sh:196
msgid "1 day ago"
msgstr "há 1 dia"
#: src/lib/common.sh:199
#, sh-printf-format
msgid "%d days ago"
msgstr "há %d dias"
#: src/lib/launch.sh:169
msgid ""
"Your password has expired. Change it on Windows or on the RD Web page, then "
"sign in again in rdfeed."
msgstr ""
"A sua senha expirou. Mude-a no Windows ou na página do RD Web e depois entre "
"de novo no rdfeed."
#: src/lib/launch.sh:171
msgid "Your account is locked. Ask your IT department."
msgstr "A sua conta está bloqueada. Fale com o seu departamento de TI."
#: src/lib/launch.sh:173
msgid "The server did not accept the password. Sign in again in rdfeed."
msgstr "O servidor não aceitou a senha. Entre de novo no rdfeed."
#: src/lib/launch.sh:175
msgid "Could not reach the server. Check the internet connection."
msgstr ""
"Não foi possível alcançar o servidor. Verifique a conexão com a internet."
#: src/lib/launch.sh:178
#, sh-printf-format
msgid "FreeRDP stopped: %s"
msgstr "O FreeRDP parou: %s"
#: src/lib/launch.sh:205 src/lib/launch.sh:206
#, sh-printf-format
msgid "File not found: %s"
msgstr "Arquivo não encontrado: %s"
#: src/lib/launch.sh:212 src/lib/launch.sh:213
msgid ""
"FreeRDP 3 is missing. Install it to start the apps (the package is called "
"freerdp)."
msgstr ""
"O FreeRDP 3 não está instalado. Instale-o para abrir os apps (o pacote se "
"chama freerdp)."
#: src/lib/launch.sh:277
#, sh-printf-format
msgid "Opening %s in %s"
msgstr "Abrindo %s no %s"
#: src/lib/launch.sh:278
#, sh-printf-format
msgid "If it does not open by itself, open it in the app from the drive %s."
msgstr "Se não abrir sozinho, abra-o no app pela unidade %s."
#: src/lib/launch.sh:280
#, sh-printf-format
msgid "Starting %s"
msgstr "Abrindo %s"
#: src/lib/launch.sh:281
msgid ""
"If your company uses MFA, confirm the sign-in in your authenticator app."
msgstr "Se a sua empresa usa MFA, confirme a entrada no seu app autenticador."
#: src/lib/launch.sh:290
msgid "A second sign-in is coming"
msgstr "Vem uma segunda entrada"
#: src/lib/launch.sh:291
#, sh-printf-format
msgid ""
"Confirm the second prompt in your authenticator app too. It comes in about "
"%d seconds."
msgstr ""
"Confirme também o segundo pedido no seu app autenticador. Ele chega em cerca "
"de %d segundos."
#: src/lib/launch.sh:301
#, sh-printf-format
msgid "%s could not start"
msgstr "Não foi possível abrir %s"
#: src/lib/launch.sh:303
#, sh-printf-format
msgid "Log: %s"
msgstr "Log: %s"
#. TRANSLATORS: the letter after "[" is the key for yes. y works too.
#: src/lib/menu.sh:93
msgid "[y/N]"
msgstr "[s/N]"
#: src/lib/menu.sh:163
msgid "1 app"
msgstr "1 app"
#: src/lib/menu.sh:165
#, sh-printf-format
msgid "%d apps"
msgstr "%d apps"
#: src/lib/menu.sh:190
msgid "No workspace yet. Add the one of your company with [1]."
msgstr "Ainda não há espaço de trabalho. Adicione o da sua empresa com [1]."
#: src/lib/menu.sh:233
msgid "off"
msgstr "desativado"
#: src/lib/menu.sh:234
msgid "home folder"
msgstr "pasta pessoal"
#: src/lib/menu.sh:237
msgid "Documents"
msgstr "Documentos"
#: src/lib/menu.sh:238
msgid "Downloads"
msgstr "Downloads"
#: src/lib/menu.sh:243
msgid "not found"
msgstr "não encontrada"
#: src/lib/menu.sh:246
#, sh-printf-format
msgid "automatic (%d %%)"
msgstr "automático (%d %%)"
#: src/lib/menu.sh:248
msgid "classic"
msgstr "clássico"
#: src/lib/menu.sh:249
msgid "GFX"
msgstr "GFX"
#: src/lib/menu.sh:250
msgid "always"
msgstr "sempre"
#: src/lib/menu.sh:252
msgid "automatic"
msgstr "automático"
#: src/lib/menu.sh:261
msgid ""
"The apps only see the files on the server. Pick a folder to open and save "
"files of this computer in them."
msgstr ""
"Os apps só veem os arquivos do servidor. Escolha uma pasta para que possam "
"abrir e salvar arquivos deste computador."
#: src/lib/menu.sh:262
msgid ""
"The apps can open and save files in this folder. It shows up as a drive in "
"them."
msgstr ""
"Os apps podem abrir e salvar arquivos nesta pasta. Ela aparece neles como "
"uma unidade."
#: src/lib/menu.sh:263
msgid ""
"Offers the apps under \"Open with\" for their file types, for example Word "
"for .docx. The folder of the file is shared with the app while it is open."
msgstr ""
"Oferece os apps em \"Abrir com\" para seus tipos de arquivo, por exemplo o "
"Word para .docx. A pasta do arquivo fica compartilhada enquanto ele está "
"aberto."
#: src/lib/menu.sh:264
msgid ""
"How big the apps are drawn. Automatic follows the scale of your desktop, so "
"they are sharp and the right size."
msgstr ""
"O tamanho em que os apps são desenhados. Automático segue a escala da sua "
"área de trabalho, assim ficam nítidos e do tamanho certo."
#: src/lib/menu.sh:265
msgid "Plays the sound of the apps here, for calls and videos."
msgstr "Toca aqui o som dos apps, para chamadas e vídeos."
#: src/lib/menu.sh:266
msgid ""
"Paints every window right. But some servers end the session right after "
"signing in, then go back to GFX."
msgstr "Desenha cada janela direito. Mas alguns servidores encerram a sessão logo após a entrada; nesse caso, volte ao GFX."
#: src/lib/menu.sh:267
msgid ""
"Works with every server. FreeRDP may not repaint a window after it was "
"minimized or resized."
msgstr "Funciona com qualquer servidor. O FreeRDP pode não redesenhar uma janela depois de minimizada ou redimensionada."
#: src/lib/menu.sh:268
msgid ""
"Always waits before the second sign-in. For when the second prompt does not "
"come with automatic."
msgstr ""
"Sempre espera antes da segunda entrada. Para quando, no automático, o "
"segundo pedido não chega."
#: src/lib/menu.sh:269
msgid "Never waits. Right when your company has no MFA."
msgstr "Nunca espera. Certo quando a sua empresa não usa MFA."
#: src/lib/menu.sh:270
msgid ""
"Many servers with MFA need a second sign-in, and its prompt only comes after "
"a short wait. Automatic waits when the first sign-in took a while."
msgstr ""
"Muitos servidores com MFA pedem uma segunda entrada, e o pedido só chega "
"depois de uma breve espera. Automático espera quando a primeira entrada "
"demorou."
#: src/lib/menu.sh:273
msgid ""
"Loads the list of apps again once a day, so new apps show up by themselves."
msgstr ""
"Carrega a lista de apps de novo uma vez por dia, assim os novos aparecem "
"sozinhos."
#: src/lib/menu.sh:275 src/lib/menu.sh:330
msgid "Needs a systemd user session."
msgstr "Precisa de uma sessão de usuário do systemd."
#: src/lib/menu.sh:322 src/lib/menu.sh:325
msgid "Could not save the setting."
msgstr "Não foi possível salvar a configuração."
#: src/lib/menu.sh:371 src/lib/menu.sh:609
msgid "Settings"
msgstr "Configurações"
#: src/lib/menu.sh:372
msgid "↑↓ select ←→ or Space: change q: back"
msgstr "↑↓ escolher ←→ ou Espaço: mudar q: voltar"
#: src/lib/menu.sh:373
msgid "ON"
msgstr "ATIVADO"
#: src/lib/menu.sh:374
msgid "OFF"
msgstr "DESATIVADO"
#: src/lib/menu.sh:456 src/lib/menu.sh:608
msgid "Workspaces"
msgstr "Espaços de trabalho"
#. TRANSLATORS: keep the letters, they are the keys.
#: src/lib/menu.sh:458
msgid ""
"↑↓ select r: refresh s: sign in again d: remove a: add q: back"
msgstr ""
"↑↓ escolher r: atualizar s: entrar de novo d: remover a: adicionar "
"q: voltar"
#: src/lib/menu.sh:459
msgid "No workspace yet. Press a to add one."
msgstr "Ainda não há espaço de trabalho. Aperte a para adicionar um."
#: src/lib/menu.sh:484
#, sh-printf-format
msgid "refreshed %s"
msgstr "atualizado %s"
#: src/lib/menu.sh:512
#, sh-printf-format
msgid "Remove \"%s\" and its apps?"
msgstr "Remover \"%s\" e seus apps?"
#: src/lib/menu.sh:536 src/lib/menu.sh:607
msgid "Start an app"
msgstr "Abrir um app"
#: src/lib/menu.sh:537
msgid "↑↓ select Enter: start q: back"
msgstr "↑↓ escolher Enter: abrir q: voltar"
#: src/lib/menu.sh:538
msgid "No apps yet. Add a workspace first."
msgstr "Ainda não há apps. Adicione primeiro um espaço de trabalho."
#: src/lib/menu.sh:579
#, sh-printf-format
msgid "%s is starting. It opens in a window of its own."
msgstr "%s está abrindo. Ele abre numa janela própria."
#: src/lib/menu.sh:606
msgid "Add a workspace"
msgstr "Adicionar um espaço de trabalho"
#: src/lib/menu.sh:610
msgid "Quit"
msgstr "Sair"
#: src/lib/workspace.sh:163
msgid "The server did not accept the user name or password."
msgstr "O servidor não aceitou o nome de usuário ou a senha."
#: src/lib/workspace.sh:164
#, sh-printf-format
msgid "Could not reach the server: %s"
msgstr "Não foi possível alcançar o servidor: %s"
#: src/lib/workspace.sh:165
#, sh-printf-format
msgid "There is no RD Web feed at %s."
msgstr "Não há feed do RD Web em %s."
#: src/lib/workspace.sh:166
#, sh-printf-format
msgid "Could not load the apps: %s"
msgstr "Não foi possível carregar os apps: %s"
#: src/lib/workspace.sh:282
#, sh-printf-format
msgid "Remote desktop of %s"
msgstr "Área de trabalho remota de %s"
#: src/lib/workspace.sh:284
#, sh-printf-format
msgid "%s from %s"
msgstr "%s de %s"
#: src/lib/menu.sh
msgid "Apps"
msgstr "Apps"
#: src/lib/menu.sh
msgid "Share a folder"
msgstr "Compartilhar uma pasta"
#: src/lib/menu.sh
msgid "Open files with the apps"
msgstr "Abrir arquivos com os apps"
#: src/lib/menu.sh
msgid "Size"
msgstr "Tamanho"
#: src/lib/menu.sh
msgid "Sound"
msgstr "Som"
#: src/lib/menu.sh
msgid "Graphics"
msgstr "Gráficos"
#: src/lib/menu.sh
msgid "Signing in"
msgstr "Acesso"
#: src/lib/menu.sh
msgid "Wait for the second MFA prompt"
msgstr "Esperar o segundo pedido de MFA"
#: src/lib/menu.sh
msgid "Refresh the apps every day"
msgstr "Atualizar os apps todo dia"
+531
View File
@@ -0,0 +1,531 @@
# Translation template for rdfeed.
# Copyright (C) 2026 Felitendo
# This file is distributed under the same license as rdfeed.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: rdfeed 1.0.0\n"
"Report-Msgid-Bugs-To: https://github.com/LoonixTools/rdfeed/issues\n"
"POT-Creation-Date: 2026-10-02 10:29+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: src/rdfeed:79 src/rdfeed:103
#, sh-printf-format
msgid "Could not write to %s."
msgstr ""
#: src/rdfeed:82
#, sh-printf-format
msgid "Signing in to %s…"
msgstr ""
#: src/rdfeed:100
msgid ""
"The password could not be saved in the keyring, so each app asks for it."
msgstr ""
#: src/rdfeed:111
#, sh-printf-format
msgid "1 app from %s is now in your app menu."
msgstr ""
#: src/rdfeed:113
#, sh-printf-format
msgid "%d apps from %s are now in your app menu."
msgstr ""
#: src/rdfeed:129
msgid ""
"Your IT department has the address. A work e-mail address often works too."
msgstr ""
#: src/rdfeed:130
msgid "Feed address, server or e-mail:"
msgstr ""
#: src/rdfeed:137
#, sh-printf-format
msgid "No feed is registered for %s. Ask your IT department for the address."
msgstr ""
#: src/rdfeed:142 src/rdfeed:156
msgid "User name (DOMAIN\\name or name@domain):"
msgstr ""
#: src/rdfeed:146 src/rdfeed:159
msgid "Password:"
msgstr ""
#: src/rdfeed:178 src/rdfeed:236
#, sh-printf-format
msgid "No workspace yet. Add one with `%s add`."
msgstr ""
#: src/rdfeed:187 src/lib/launch.sh:220
#, sh-printf-format
msgid "No password saved for %s. Sign in again in rdfeed."
msgstr ""
#: src/rdfeed:191 src/lib/launch.sh:149
#, sh-printf-format
msgid "Password for %s:"
msgstr ""
#: src/rdfeed:195
#, sh-printf-format
msgid "Refreshing %s…"
msgstr ""
#: src/rdfeed:205
msgid "The password no longer works. Sign in again in rdfeed."
msgstr ""
#: src/rdfeed:214
#, sh-printf-format
msgid "%s: %d apps"
msgstr ""
#: src/rdfeed:222
#, sh-printf-format
msgid "Which workspace? See `%s list` for the names."
msgstr ""
#: src/rdfeed:229
#, sh-printf-format
msgid "%s and its apps are removed."
msgstr ""
#: src/rdfeed:253
#, sh-printf-format
msgid "Which app? See `%s list`."
msgstr ""
#: src/rdfeed:268 src/lib/launch.sh:199
#, sh-printf-format
msgid "There is no app %s. See `%s list`."
msgstr ""
#: src/rdfeed:270
#, sh-printf-format
msgid "Several workspaces have %s. Write it as WORKSPACE/APP, see `%s list`."
msgstr ""
#: src/rdfeed:278
msgid "Usage: rdfeed [command]"
msgstr ""
#: src/rdfeed:280
msgid "Commands:"
msgstr ""
#: src/rdfeed:281
msgid "Add the workspace of your company"
msgstr ""
#: src/rdfeed:282
msgid "Load the apps again"
msgstr ""
#: src/rdfeed:283
msgid "List the workspaces and their apps"
msgstr ""
#: src/rdfeed:284
msgid "Start an app, or open a file with it"
msgstr ""
#: src/rdfeed:285
msgid "Remove a workspace and its apps"
msgstr ""
#: src/rdfeed:286
msgid "Show this help"
msgstr ""
#: src/rdfeed:287
msgid "Show the version"
msgstr ""
#: src/rdfeed:289
msgid "Without a command an interactive menu is shown."
msgstr ""
#: src/rdfeed:313
#, sh-printf-format
msgid "Unknown command: %s"
msgstr ""
#: src/lib/common.sh:174 src/lib/menu.sh:251
msgid "never"
msgstr ""
#: src/lib/common.sh:184
msgid "just now"
msgstr ""
#: src/lib/common.sh:186
msgid "1 minute ago"
msgstr ""
#: src/lib/common.sh:189
#, sh-printf-format
msgid "%d minutes ago"
msgstr ""
#: src/lib/common.sh:191
msgid "1 hour ago"
msgstr ""
#: src/lib/common.sh:194
#, sh-printf-format
msgid "%d hours ago"
msgstr ""
#: src/lib/common.sh:196
msgid "1 day ago"
msgstr ""
#: src/lib/common.sh:199
#, sh-printf-format
msgid "%d days ago"
msgstr ""
#: src/lib/launch.sh:169
msgid ""
"Your password has expired. Change it on Windows or on the RD Web page, then "
"sign in again in rdfeed."
msgstr ""
#: src/lib/launch.sh:171
msgid "Your account is locked. Ask your IT department."
msgstr ""
#: src/lib/launch.sh:173
msgid "The server did not accept the password. Sign in again in rdfeed."
msgstr ""
#: src/lib/launch.sh:175
msgid "Could not reach the server. Check the internet connection."
msgstr ""
#: src/lib/launch.sh:178
#, sh-printf-format
msgid "FreeRDP stopped: %s"
msgstr ""
#: src/lib/launch.sh:205 src/lib/launch.sh:206
#, sh-printf-format
msgid "File not found: %s"
msgstr ""
#: src/lib/launch.sh:212 src/lib/launch.sh:213
msgid ""
"FreeRDP 3 is missing. Install it to start the apps (the package is called "
"freerdp)."
msgstr ""
#: src/lib/launch.sh:277
#, sh-printf-format
msgid "Opening %s in %s"
msgstr ""
#: src/lib/launch.sh:278
#, sh-printf-format
msgid "If it does not open by itself, open it in the app from the drive %s."
msgstr ""
#: src/lib/launch.sh:280
#, sh-printf-format
msgid "Starting %s"
msgstr ""
#: src/lib/launch.sh:281
msgid ""
"If your company uses MFA, confirm the sign-in in your authenticator app."
msgstr ""
#: src/lib/launch.sh:290
msgid "A second sign-in is coming"
msgstr ""
#: src/lib/launch.sh:291
#, sh-printf-format
msgid ""
"Confirm the second prompt in your authenticator app too. It comes in about "
"%d seconds."
msgstr ""
#: src/lib/launch.sh:301
#, sh-printf-format
msgid "%s could not start"
msgstr ""
#: src/lib/launch.sh:303
#, sh-printf-format
msgid "Log: %s"
msgstr ""
#. TRANSLATORS: the letter after "[" is the key for yes. y works too.
#: src/lib/menu.sh:93
msgid "[y/N]"
msgstr ""
#: src/lib/menu.sh:163
msgid "1 app"
msgstr ""
#: src/lib/menu.sh:165
#, sh-printf-format
msgid "%d apps"
msgstr ""
#: src/lib/menu.sh:190
msgid "No workspace yet. Add the one of your company with [1]."
msgstr ""
#: src/lib/menu.sh:233
msgid "off"
msgstr ""
#: src/lib/menu.sh:234
msgid "home folder"
msgstr ""
#: src/lib/menu.sh:237
msgid "Documents"
msgstr ""
#: src/lib/menu.sh:238
msgid "Downloads"
msgstr ""
#: src/lib/menu.sh:243
msgid "not found"
msgstr ""
#: src/lib/menu.sh:246
#, sh-printf-format
msgid "automatic (%d %%)"
msgstr ""
#: src/lib/menu.sh:248
msgid "classic"
msgstr ""
#: src/lib/menu.sh:249
msgid "GFX"
msgstr ""
#: src/lib/menu.sh:250
msgid "always"
msgstr ""
#: src/lib/menu.sh:252
msgid "automatic"
msgstr ""
#: src/lib/menu.sh:261
msgid ""
"The apps only see the files on the server. Pick a folder to open and save "
"files of this computer in them."
msgstr ""
#: src/lib/menu.sh:262
msgid ""
"The apps can open and save files in this folder. It shows up as a drive in "
"them."
msgstr ""
#: src/lib/menu.sh:263
msgid ""
"Offers the apps under \"Open with\" for their file types, for example Word "
"for .docx. The folder of the file is shared with the app while it is open."
msgstr ""
#: src/lib/menu.sh:264
msgid ""
"How big the apps are drawn. Automatic follows the scale of your desktop, so "
"they are sharp and the right size."
msgstr ""
#: src/lib/menu.sh:265
msgid "Plays the sound of the apps here, for calls and videos."
msgstr ""
#: src/lib/menu.sh:266
msgid ""
"Paints every window right. But some servers end the session right after "
"signing in, then go back to GFX."
msgstr ""
#: src/lib/menu.sh:267
msgid ""
"Works with every server. FreeRDP may not repaint a window after it was "
"minimized or resized."
msgstr ""
#: src/lib/menu.sh:268
msgid ""
"Always waits before the second sign-in. For when the second prompt does not "
"come with automatic."
msgstr ""
#: src/lib/menu.sh:269
msgid "Never waits. Right when your company has no MFA."
msgstr ""
#: src/lib/menu.sh:270
msgid ""
"Many servers with MFA need a second sign-in, and its prompt only comes after "
"a short wait. Automatic waits when the first sign-in took a while."
msgstr ""
#: src/lib/menu.sh:273
msgid ""
"Loads the list of apps again once a day, so new apps show up by themselves."
msgstr ""
#: src/lib/menu.sh:275 src/lib/menu.sh:330
msgid "Needs a systemd user session."
msgstr ""
#: src/lib/menu.sh:322 src/lib/menu.sh:325
msgid "Could not save the setting."
msgstr ""
#: src/lib/menu.sh:371 src/lib/menu.sh:609
msgid "Settings"
msgstr ""
#: src/lib/menu.sh:372
msgid "↑↓ select ←→ or Space: change q: back"
msgstr ""
#: src/lib/menu.sh:373
msgid "ON"
msgstr ""
#: src/lib/menu.sh:374
msgid "OFF"
msgstr ""
#: src/lib/menu.sh:456 src/lib/menu.sh:608
msgid "Workspaces"
msgstr ""
#. TRANSLATORS: keep the letters, they are the keys.
#: src/lib/menu.sh:458
msgid ""
"↑↓ select r: refresh s: sign in again d: remove a: add q: back"
msgstr ""
#: src/lib/menu.sh:459
msgid "No workspace yet. Press a to add one."
msgstr ""
#: src/lib/menu.sh:484
#, sh-printf-format
msgid "refreshed %s"
msgstr ""
#: src/lib/menu.sh:512
#, sh-printf-format
msgid "Remove \"%s\" and its apps?"
msgstr ""
#: src/lib/menu.sh:536 src/lib/menu.sh:607
msgid "Start an app"
msgstr ""
#: src/lib/menu.sh:537
msgid "↑↓ select Enter: start q: back"
msgstr ""
#: src/lib/menu.sh:538
msgid "No apps yet. Add a workspace first."
msgstr ""
#: src/lib/menu.sh:579
#, sh-printf-format
msgid "%s is starting. It opens in a window of its own."
msgstr ""
#: src/lib/menu.sh:606
msgid "Add a workspace"
msgstr ""
#: src/lib/menu.sh:610
msgid "Quit"
msgstr ""
#: src/lib/workspace.sh:163
msgid "The server did not accept the user name or password."
msgstr ""
#: src/lib/workspace.sh:164
#, sh-printf-format
msgid "Could not reach the server: %s"
msgstr ""
#: src/lib/workspace.sh:165
#, sh-printf-format
msgid "There is no RD Web feed at %s."
msgstr ""
#: src/lib/workspace.sh:166
#, sh-printf-format
msgid "Could not load the apps: %s"
msgstr ""
#: src/lib/workspace.sh:282
#, sh-printf-format
msgid "Remote desktop of %s"
msgstr ""
#: src/lib/workspace.sh:284
#, sh-printf-format
msgid "%s from %s"
msgstr ""
#: src/lib/menu.sh
msgid "Apps"
msgstr ""
#: src/lib/menu.sh
msgid "Share a folder"
msgstr ""
#: src/lib/menu.sh
msgid "Open files with the apps"
msgstr ""
#: src/lib/menu.sh
msgid "Size"
msgstr ""
#: src/lib/menu.sh
msgid "Sound"
msgstr ""
#: src/lib/menu.sh
msgid "Graphics"
msgstr ""
#: src/lib/menu.sh
msgid "Signing in"
msgstr ""
#: src/lib/menu.sh
msgid "Wait for the second MFA prompt"
msgstr ""
#: src/lib/menu.sh
msgid "Refresh the apps every day"
msgstr ""
+558
View File
@@ -0,0 +1,558 @@
# Russian translation for rdfeed.
# Copyright (C) 2026 Felitendo
# This file is distributed under the same license as rdfeed.
#
msgid ""
msgstr ""
"Project-Id-Version: rdfeed 1.0.0\n"
"Report-Msgid-Bugs-To: https://github.com/LoonixTools/rdfeed/issues\n"
"POT-Creation-Date: 2026-10-02 10:29+0200\n"
"PO-Revision-Date: 2026-10-02 09:30+0200\n"
"Last-Translator: Felitendo\n"
"Language-Team: Russian\n"
"Language: ru\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: src/rdfeed:79 src/rdfeed:103
#, sh-printf-format
msgid "Could not write to %s."
msgstr "Не удалось записать в %s."
#: src/rdfeed:82
#, sh-printf-format
msgid "Signing in to %s…"
msgstr "Вход на %s…"
#: src/rdfeed:100
msgid ""
"The password could not be saved in the keyring, so each app asks for it."
msgstr ""
"Не удалось сохранить пароль в связке ключей, поэтому каждое приложение будет "
"его запрашивать."
#: src/rdfeed:111
#, sh-printf-format
msgid "1 app from %s is now in your app menu."
msgstr "1 приложение из %s теперь в меню приложений."
#: src/rdfeed:113
#, sh-printf-format
msgid "%d apps from %s are now in your app menu."
msgstr "Приложений: %d (из %s). Теперь они в меню приложений."
#: src/rdfeed:129
msgid ""
"Your IT department has the address. A work e-mail address often works too."
msgstr ""
"Адрес есть у вашего ИТ-отдела. Часто подходит и рабочий адрес электронной "
"почты."
#: src/rdfeed:130
msgid "Feed address, server or e-mail:"
msgstr "Адрес фида, сервер или e-mail:"
#: src/rdfeed:137
#, sh-printf-format
msgid "No feed is registered for %s. Ask your IT department for the address."
msgstr "Для %s не зарегистрирован фид. Спросите адрес у ИТ-отдела."
#: src/rdfeed:142 src/rdfeed:156
msgid "User name (DOMAIN\\name or name@domain):"
msgstr "Имя пользователя (ДОМЕН\\имя или имя@домен):"
#: src/rdfeed:146 src/rdfeed:159
msgid "Password:"
msgstr "Пароль:"
#: src/rdfeed:178 src/rdfeed:236
#, sh-printf-format
msgid "No workspace yet. Add one with `%s add`."
msgstr "Рабочих областей пока нет. Добавьте её командой `%s add`."
#: src/rdfeed:187 src/lib/launch.sh:220
#, sh-printf-format
msgid "No password saved for %s. Sign in again in rdfeed."
msgstr "Для %s не сохранён пароль. Войдите снова в rdfeed."
#: src/rdfeed:191 src/lib/launch.sh:149
#, sh-printf-format
msgid "Password for %s:"
msgstr "Пароль для %s:"
#: src/rdfeed:195
#, sh-printf-format
msgid "Refreshing %s…"
msgstr "Обновление %s…"
#: src/rdfeed:205
msgid "The password no longer works. Sign in again in rdfeed."
msgstr "Пароль больше не подходит. Войдите снова в rdfeed."
#: src/rdfeed:214
#, sh-printf-format
msgid "%s: %d apps"
msgstr "%s: приложений: %d"
#: src/rdfeed:222
#, sh-printf-format
msgid "Which workspace? See `%s list` for the names."
msgstr "Какая рабочая область? Названия показывает `%s list`."
#: src/rdfeed:229
#, sh-printf-format
msgid "%s and its apps are removed."
msgstr "Удалено: %s вместе с приложениями."
#: src/rdfeed:253
#, sh-printf-format
msgid "Which app? See `%s list`."
msgstr "Какое приложение? Смотрите `%s list`."
#: src/rdfeed:268 src/lib/launch.sh:199
#, sh-printf-format
msgid "There is no app %s. See `%s list`."
msgstr "Приложения %s нет. Смотрите `%s list`."
#: src/rdfeed:270
#, sh-printf-format
msgid "Several workspaces have %s. Write it as WORKSPACE/APP, see `%s list`."
msgstr ""
"%s есть в нескольких рабочих областях. Укажите в виде ОБЛАСТЬ/ПРИЛОЖЕНИЕ, "
"смотрите `%s list`."
#: src/rdfeed:278
msgid "Usage: rdfeed [command]"
msgstr "Использование: rdfeed [команда]"
#: src/rdfeed:280
msgid "Commands:"
msgstr "Команды:"
#: src/rdfeed:281
msgid "Add the workspace of your company"
msgstr "Добавить рабочую область вашей компании"
#: src/rdfeed:282
msgid "Load the apps again"
msgstr "Загрузить приложения заново"
#: src/rdfeed:283
msgid "List the workspaces and their apps"
msgstr "Показать рабочие области и их приложения"
#: src/rdfeed:284
msgid "Start an app, or open a file with it"
msgstr "Запустить приложение или открыть в нём файл"
#: src/rdfeed:285
msgid "Remove a workspace and its apps"
msgstr "Удалить рабочую область и её приложения"
#: src/rdfeed:286
msgid "Show this help"
msgstr "Показать эту справку"
#: src/rdfeed:287
msgid "Show the version"
msgstr "Показать версию"
#: src/rdfeed:289
msgid "Without a command an interactive menu is shown."
msgstr "Без команды открывается интерактивное меню."
#: src/rdfeed:313
#, sh-printf-format
msgid "Unknown command: %s"
msgstr "Неизвестная команда: %s"
#: src/lib/common.sh:174 src/lib/menu.sh:251
msgid "never"
msgstr "никогда"
#: src/lib/common.sh:184
msgid "just now"
msgstr "только что"
#: src/lib/common.sh:186
msgid "1 minute ago"
msgstr "1 минуту назад"
#: src/lib/common.sh:189
#, sh-printf-format
msgid "%d minutes ago"
msgstr "%d мин назад"
#: src/lib/common.sh:191
msgid "1 hour ago"
msgstr "1 час назад"
#: src/lib/common.sh:194
#, sh-printf-format
msgid "%d hours ago"
msgstr "%d ч назад"
#: src/lib/common.sh:196
msgid "1 day ago"
msgstr "1 день назад"
#: src/lib/common.sh:199
#, sh-printf-format
msgid "%d days ago"
msgstr "%d дн. назад"
#: src/lib/launch.sh:169
msgid ""
"Your password has expired. Change it on Windows or on the RD Web page, then "
"sign in again in rdfeed."
msgstr ""
"Срок действия пароля истёк. Смените его в Windows или на странице RD Web, "
"затем снова войдите в rdfeed."
#: src/lib/launch.sh:171
msgid "Your account is locked. Ask your IT department."
msgstr "Учётная запись заблокирована. Обратитесь в ИТ-отдел."
#: src/lib/launch.sh:173
msgid "The server did not accept the password. Sign in again in rdfeed."
msgstr "Сервер не принял пароль. Войдите снова в rdfeed."
#: src/lib/launch.sh:175
msgid "Could not reach the server. Check the internet connection."
msgstr "Не удалось связаться с сервером. Проверьте подключение к интернету."
#: src/lib/launch.sh:178
#, sh-printf-format
msgid "FreeRDP stopped: %s"
msgstr "FreeRDP остановился: %s"
#: src/lib/launch.sh:205 src/lib/launch.sh:206
#, sh-printf-format
msgid "File not found: %s"
msgstr "Файл не найден: %s"
#: src/lib/launch.sh:212 src/lib/launch.sh:213
msgid ""
"FreeRDP 3 is missing. Install it to start the apps (the package is called "
"freerdp)."
msgstr ""
"Нет FreeRDP 3. Установите его, чтобы запускать приложения (пакет называется "
"freerdp)."
#: src/lib/launch.sh:277
#, sh-printf-format
msgid "Opening %s in %s"
msgstr "Открытие %s в %s"
#: src/lib/launch.sh:278
#, sh-printf-format
msgid "If it does not open by itself, open it in the app from the drive %s."
msgstr "Если файл не откроется сам, откройте его в приложении с диска %s."
#: src/lib/launch.sh:280
#, sh-printf-format
msgid "Starting %s"
msgstr "Запуск: %s"
#: src/lib/launch.sh:281
msgid ""
"If your company uses MFA, confirm the sign-in in your authenticator app."
msgstr ""
"Если ваша компания использует MFA, подтвердите вход в приложении-"
"аутентификаторе."
#: src/lib/launch.sh:290
msgid "A second sign-in is coming"
msgstr "Будет второй вход"
#: src/lib/launch.sh:291
#, sh-printf-format
msgid ""
"Confirm the second prompt in your authenticator app too. It comes in about "
"%d seconds."
msgstr ""
"Подтвердите и второй запрос в приложении-аутентификаторе. Он придёт примерно "
"через %d с."
#: src/lib/launch.sh:301
#, sh-printf-format
msgid "%s could not start"
msgstr "Не удалось запустить: %s"
#: src/lib/launch.sh:303
#, sh-printf-format
msgid "Log: %s"
msgstr "Журнал: %s"
#. TRANSLATORS: the letter after "[" is the key for yes. y works too.
#: src/lib/menu.sh:93
msgid "[y/N]"
msgstr "[д/Н]"
#: src/lib/menu.sh:163
msgid "1 app"
msgstr "1 приложение"
#: src/lib/menu.sh:165
#, sh-printf-format
msgid "%d apps"
msgstr "приложений: %d"
#: src/lib/menu.sh:190
msgid "No workspace yet. Add the one of your company with [1]."
msgstr "Рабочих областей пока нет. Добавьте область вашей компании через [1]."
#: src/lib/menu.sh:233
msgid "off"
msgstr "выкл."
#: src/lib/menu.sh:234
msgid "home folder"
msgstr "домашняя папка"
#: src/lib/menu.sh:237
msgid "Documents"
msgstr "Документы"
#: src/lib/menu.sh:238
msgid "Downloads"
msgstr "Загрузки"
#: src/lib/menu.sh:243
msgid "not found"
msgstr "не найдена"
#: src/lib/menu.sh:246
#, sh-printf-format
msgid "automatic (%d %%)"
msgstr "автоматически (%d %%)"
#: src/lib/menu.sh:248
msgid "classic"
msgstr "классический"
#: src/lib/menu.sh:249
msgid "GFX"
msgstr "GFX"
#: src/lib/menu.sh:250
msgid "always"
msgstr "всегда"
#: src/lib/menu.sh:252
msgid "automatic"
msgstr "автоматически"
#: src/lib/menu.sh:261
msgid ""
"The apps only see the files on the server. Pick a folder to open and save "
"files of this computer in them."
msgstr ""
"Приложения видят только файлы на сервере. Выберите папку, чтобы они могли "
"открывать и сохранять файлы этого компьютера."
#: src/lib/menu.sh:262
msgid ""
"The apps can open and save files in this folder. It shows up as a drive in "
"them."
msgstr ""
"Приложения могут открывать и сохранять файлы в этой папке. В них она видна "
"как диск."
#: src/lib/menu.sh:263
msgid ""
"Offers the apps under \"Open with\" for their file types, for example Word "
"for .docx. The folder of the file is shared with the app while it is open."
msgstr ""
"Предлагает приложения в «Открыть с помощью» для их типов файлов, например "
"Word для .docx. Папка файла открыта для сервера, пока файл открыт."
#: src/lib/menu.sh:264
msgid ""
"How big the apps are drawn. Automatic follows the scale of your desktop, so "
"they are sharp and the right size."
msgstr ""
"Насколько крупно рисуются приложения. «Автоматически» следует масштабу "
"рабочего стола, так они чёткие и нужного размера."
#: src/lib/menu.sh:265
msgid "Plays the sound of the apps here, for calls and videos."
msgstr "Воспроизводит звук приложений здесь, для звонков и видео."
#: src/lib/menu.sh:266
msgid ""
"Paints every window right. But some servers end the session right after "
"signing in, then go back to GFX."
msgstr "Правильно рисует каждое окно. Но некоторые серверы обрывают сеанс сразу после входа, тогда вернитесь к GFX."
#: src/lib/menu.sh:267
msgid ""
"Works with every server. FreeRDP may not repaint a window after it was "
"minimized or resized."
msgstr "Работает с любым сервером. FreeRDP может не перерисовать окно после сворачивания или изменения размера."
#: src/lib/menu.sh:268
msgid ""
"Always waits before the second sign-in. For when the second prompt does not "
"come with automatic."
msgstr ""
"Всегда ждёт перед вторым входом. Если с «автоматически» второй запрос не "
"приходит."
#: src/lib/menu.sh:269
msgid "Never waits. Right when your company has no MFA."
msgstr "Никогда не ждёт. Подходит, если ваша компания не использует MFA."
#: src/lib/menu.sh:270
msgid ""
"Many servers with MFA need a second sign-in, and its prompt only comes after "
"a short wait. Automatic waits when the first sign-in took a while."
msgstr ""
"Многим серверам с MFA нужен второй вход, и его запрос приходит только после "
"короткой паузы. «Автоматически» ждёт, если первый вход занял время."
#: src/lib/menu.sh:273
msgid ""
"Loads the list of apps again once a day, so new apps show up by themselves."
msgstr ""
"Раз в день заново загружает список приложений, так новые появляются сами."
#: src/lib/menu.sh:275 src/lib/menu.sh:330
msgid "Needs a systemd user session."
msgstr "Нужен пользовательский сеанс systemd."
#: src/lib/menu.sh:322 src/lib/menu.sh:325
msgid "Could not save the setting."
msgstr "Не удалось сохранить настройку."
#: src/lib/menu.sh:371 src/lib/menu.sh:609
msgid "Settings"
msgstr "Настройки"
#: src/lib/menu.sh:372
msgid "↑↓ select ←→ or Space: change q: back"
msgstr "↑↓ выбор ←→ или Пробел: изменить q: назад"
#: src/lib/menu.sh:373
msgid "ON"
msgstr "ВКЛ"
#: src/lib/menu.sh:374
msgid "OFF"
msgstr "ВЫКЛ"
#: src/lib/menu.sh:456 src/lib/menu.sh:608
msgid "Workspaces"
msgstr "Рабочие области"
#. TRANSLATORS: keep the letters, they are the keys.
#: src/lib/menu.sh:458
msgid ""
"↑↓ select r: refresh s: sign in again d: remove a: add q: back"
msgstr ""
"↑↓ выбор r: обновить s: войти снова d: удалить a: добавить q: назад"
#: src/lib/menu.sh:459
msgid "No workspace yet. Press a to add one."
msgstr "Рабочих областей пока нет. Нажмите a, чтобы добавить."
#: src/lib/menu.sh:484
#, sh-printf-format
msgid "refreshed %s"
msgstr "обновлено %s"
#: src/lib/menu.sh:512
#, sh-printf-format
msgid "Remove \"%s\" and its apps?"
msgstr "Удалить «%s» вместе с приложениями?"
#: src/lib/menu.sh:536 src/lib/menu.sh:607
msgid "Start an app"
msgstr "Запустить приложение"
#: src/lib/menu.sh:537
msgid "↑↓ select Enter: start q: back"
msgstr "↑↓ выбор Enter: запустить q: назад"
#: src/lib/menu.sh:538
msgid "No apps yet. Add a workspace first."
msgstr "Приложений пока нет. Сначала добавьте рабочую область."
#: src/lib/menu.sh:579
#, sh-printf-format
msgid "%s is starting. It opens in a window of its own."
msgstr "Запуск: %s. Откроется в отдельном окне."
#: src/lib/menu.sh:606
msgid "Add a workspace"
msgstr "Добавить рабочую область"
#: src/lib/menu.sh:610
msgid "Quit"
msgstr "Выход"
#: src/lib/workspace.sh:163
msgid "The server did not accept the user name or password."
msgstr "Сервер не принял имя пользователя или пароль."
#: src/lib/workspace.sh:164
#, sh-printf-format
msgid "Could not reach the server: %s"
msgstr "Не удалось связаться с сервером: %s"
#: src/lib/workspace.sh:165
#, sh-printf-format
msgid "There is no RD Web feed at %s."
msgstr "По адресу %s нет фида RD Web."
#: src/lib/workspace.sh:166
#, sh-printf-format
msgid "Could not load the apps: %s"
msgstr "Не удалось загрузить приложения: %s"
#: src/lib/workspace.sh:282
#, sh-printf-format
msgid "Remote desktop of %s"
msgstr "Удалённый рабочий стол %s"
#: src/lib/workspace.sh:284
#, sh-printf-format
msgid "%s from %s"
msgstr "%s из %s"
#: src/lib/menu.sh
msgid "Apps"
msgstr "Приложения"
#: src/lib/menu.sh
msgid "Share a folder"
msgstr "Общая папка"
#: src/lib/menu.sh
msgid "Open files with the apps"
msgstr "Открывать файлы в приложениях"
#: src/lib/menu.sh
msgid "Size"
msgstr "Размер"
#: src/lib/menu.sh
msgid "Sound"
msgstr "Звук"
#: src/lib/menu.sh
msgid "Graphics"
msgstr "Графика"
#: src/lib/menu.sh
msgid "Signing in"
msgstr "Вход"
#: src/lib/menu.sh
msgid "Wait for the second MFA prompt"
msgstr "Ждать второй запрос MFA"
#: src/lib/menu.sh
msgid "Refresh the apps every day"
msgstr "Обновлять приложения каждый день"
+554
View File
@@ -0,0 +1,554 @@
# Turkish translation for rdfeed.
# Copyright (C) 2026 Felitendo
# This file is distributed under the same license as rdfeed.
#
msgid ""
msgstr ""
"Project-Id-Version: rdfeed 1.0.0\n"
"Report-Msgid-Bugs-To: https://github.com/LoonixTools/rdfeed/issues\n"
"POT-Creation-Date: 2026-10-02 10:29+0200\n"
"PO-Revision-Date: 2026-10-02 09:30+0200\n"
"Last-Translator: Felitendo\n"
"Language-Team: Turkish\n"
"Language: tr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: src/rdfeed:79 src/rdfeed:103
#, sh-printf-format
msgid "Could not write to %s."
msgstr "%s konumuna yazılamadı."
#: src/rdfeed:82
#, sh-printf-format
msgid "Signing in to %s…"
msgstr "%s üzerinde oturum açılıyor…"
#: src/rdfeed:100
msgid ""
"The password could not be saved in the keyring, so each app asks for it."
msgstr ""
"Parola anahtarlığa kaydedilemedi, bu yüzden her uygulama parolayı soracak."
#: src/rdfeed:111
#, sh-printf-format
msgid "1 app from %s is now in your app menu."
msgstr "%s kaynağından 1 uygulama artık uygulama menünüzde."
#: src/rdfeed:113
#, sh-printf-format
msgid "%d apps from %s are now in your app menu."
msgstr "%d uygulama (%s) artık uygulama menünüzde."
#: src/rdfeed:129
msgid ""
"Your IT department has the address. A work e-mail address often works too."
msgstr "Adres BT bölümünüzde. Çoğu zaman iş e-posta adresiniz de işe yarar."
#: src/rdfeed:130
msgid "Feed address, server or e-mail:"
msgstr "Akış adresi, sunucu ya da e-posta:"
#: src/rdfeed:137
#, sh-printf-format
msgid "No feed is registered for %s. Ask your IT department for the address."
msgstr "%s için kayıtlı bir akış yok. Adresi BT bölümünüze sorun."
#: src/rdfeed:142 src/rdfeed:156
msgid "User name (DOMAIN\\name or name@domain):"
msgstr "Kullanıcı adı (ALAN\\ad ya da ad@alan):"
#: src/rdfeed:146 src/rdfeed:159
msgid "Password:"
msgstr "Parola:"
#: src/rdfeed:178 src/rdfeed:236
#, sh-printf-format
msgid "No workspace yet. Add one with `%s add`."
msgstr "Henüz çalışma alanı yok. `%s add` ile bir tane ekleyin."
#: src/rdfeed:187 src/lib/launch.sh:220
#, sh-printf-format
msgid "No password saved for %s. Sign in again in rdfeed."
msgstr "%s için kayıtlı parola yok. rdfeed içinde yeniden oturum açın."
#: src/rdfeed:191 src/lib/launch.sh:149
#, sh-printf-format
msgid "Password for %s:"
msgstr "%s parolası:"
#: src/rdfeed:195
#, sh-printf-format
msgid "Refreshing %s…"
msgstr "%s yenileniyor…"
#: src/rdfeed:205
msgid "The password no longer works. Sign in again in rdfeed."
msgstr "Parola artık çalışmıyor. rdfeed içinde yeniden oturum açın."
#: src/rdfeed:214
#, sh-printf-format
msgid "%s: %d apps"
msgstr "%s: %d uygulama"
#: src/rdfeed:222
#, sh-printf-format
msgid "Which workspace? See `%s list` for the names."
msgstr "Hangi çalışma alanı? Adları `%s list` gösterir."
#: src/rdfeed:229
#, sh-printf-format
msgid "%s and its apps are removed."
msgstr "%s ve uygulamaları kaldırıldı."
#: src/rdfeed:253
#, sh-printf-format
msgid "Which app? See `%s list`."
msgstr "Hangi uygulama? `%s list` komutuna bakın."
#: src/rdfeed:268 src/lib/launch.sh:199
#, sh-printf-format
msgid "There is no app %s. See `%s list`."
msgstr "%s diye bir uygulama yok. `%s list` komutuna bakın."
#: src/rdfeed:270
#, sh-printf-format
msgid "Several workspaces have %s. Write it as WORKSPACE/APP, see `%s list`."
msgstr ""
"%s birden çok çalışma alanında var. ÇALIŞMA_ALANI/UYGULAMA biçiminde yazın, "
"`%s list` komutuna bakın."
#: src/rdfeed:278
msgid "Usage: rdfeed [command]"
msgstr "Kullanım: rdfeed [komut]"
#: src/rdfeed:280
msgid "Commands:"
msgstr "Komutlar:"
#: src/rdfeed:281
msgid "Add the workspace of your company"
msgstr "Şirketinizin çalışma alanını ekle"
#: src/rdfeed:282
msgid "Load the apps again"
msgstr "Uygulamaları yeniden yükle"
#: src/rdfeed:283
msgid "List the workspaces and their apps"
msgstr "Çalışma alanlarını ve uygulamalarını listele"
#: src/rdfeed:284
msgid "Start an app, or open a file with it"
msgstr "Bir uygulama başlat ya da onunla bir dosya aç"
#: src/rdfeed:285
msgid "Remove a workspace and its apps"
msgstr "Bir çalışma alanını ve uygulamalarını kaldır"
#: src/rdfeed:286
msgid "Show this help"
msgstr "Bu yardımı göster"
#: src/rdfeed:287
msgid "Show the version"
msgstr "Sürümü göster"
#: src/rdfeed:289
msgid "Without a command an interactive menu is shown."
msgstr "Komut verilmezse etkileşimli bir menü gösterilir."
#: src/rdfeed:313
#, sh-printf-format
msgid "Unknown command: %s"
msgstr "Bilinmeyen komut: %s"
#: src/lib/common.sh:174 src/lib/menu.sh:251
msgid "never"
msgstr "hiçbir zaman"
#: src/lib/common.sh:184
msgid "just now"
msgstr "az önce"
#: src/lib/common.sh:186
msgid "1 minute ago"
msgstr "1 dakika önce"
#: src/lib/common.sh:189
#, sh-printf-format
msgid "%d minutes ago"
msgstr "%d dakika önce"
#: src/lib/common.sh:191
msgid "1 hour ago"
msgstr "1 saat önce"
#: src/lib/common.sh:194
#, sh-printf-format
msgid "%d hours ago"
msgstr "%d saat önce"
#: src/lib/common.sh:196
msgid "1 day ago"
msgstr "1 gün önce"
#: src/lib/common.sh:199
#, sh-printf-format
msgid "%d days ago"
msgstr "%d gün önce"
#: src/lib/launch.sh:169
msgid ""
"Your password has expired. Change it on Windows or on the RD Web page, then "
"sign in again in rdfeed."
msgstr ""
"Parolanızın süresi doldu. Windows'ta ya da RD Web sayfasında değiştirin, "
"sonra rdfeed içinde yeniden oturum açın."
#: src/lib/launch.sh:171
msgid "Your account is locked. Ask your IT department."
msgstr "Hesabınız kilitli. BT bölümünüze sorun."
#: src/lib/launch.sh:173
msgid "The server did not accept the password. Sign in again in rdfeed."
msgstr "Sunucu parolayı kabul etmedi. rdfeed içinde yeniden oturum açın."
#: src/lib/launch.sh:175
msgid "Could not reach the server. Check the internet connection."
msgstr "Sunucuya ulaşılamadı. İnternet bağlantısını kontrol edin."
#: src/lib/launch.sh:178
#, sh-printf-format
msgid "FreeRDP stopped: %s"
msgstr "FreeRDP durdu: %s"
#: src/lib/launch.sh:205 src/lib/launch.sh:206
#, sh-printf-format
msgid "File not found: %s"
msgstr "Dosya bulunamadı: %s"
#: src/lib/launch.sh:212 src/lib/launch.sh:213
msgid ""
"FreeRDP 3 is missing. Install it to start the apps (the package is called "
"freerdp)."
msgstr ""
"FreeRDP 3 eksik. Uygulamaları başlatmak için kurun (paketin adı freerdp)."
#: src/lib/launch.sh:277
#, sh-printf-format
msgid "Opening %s in %s"
msgstr "%s, %s ile açılıyor"
#: src/lib/launch.sh:278
#, sh-printf-format
msgid "If it does not open by itself, open it in the app from the drive %s."
msgstr "Kendiliğinden açılmazsa uygulamada %s sürücüsünden açın."
#: src/lib/launch.sh:280
#, sh-printf-format
msgid "Starting %s"
msgstr "%s başlatılıyor"
#: src/lib/launch.sh:281
msgid ""
"If your company uses MFA, confirm the sign-in in your authenticator app."
msgstr ""
"Şirketiniz MFA kullanıyorsa doğrulama uygulamanızda oturum açmayı onaylayın."
#: src/lib/launch.sh:290
msgid "A second sign-in is coming"
msgstr "İkinci bir oturum açma geliyor"
#: src/lib/launch.sh:291
#, sh-printf-format
msgid ""
"Confirm the second prompt in your authenticator app too. It comes in about "
"%d seconds."
msgstr ""
"İkinci isteği de doğrulama uygulamanızda onaylayın. Yaklaşık %d saniye "
"içinde gelir."
#: src/lib/launch.sh:301
#, sh-printf-format
msgid "%s could not start"
msgstr "%s başlatılamadı"
#: src/lib/launch.sh:303
#, sh-printf-format
msgid "Log: %s"
msgstr "Günlük: %s"
#. TRANSLATORS: the letter after "[" is the key for yes. y works too.
#: src/lib/menu.sh:93
msgid "[y/N]"
msgstr "[e/H]"
#: src/lib/menu.sh:163
msgid "1 app"
msgstr "1 uygulama"
#: src/lib/menu.sh:165
#, sh-printf-format
msgid "%d apps"
msgstr "%d uygulama"
#: src/lib/menu.sh:190
msgid "No workspace yet. Add the one of your company with [1]."
msgstr "Henüz çalışma alanı yok. Şirketinizinkini [1] ile ekleyin."
#: src/lib/menu.sh:233
msgid "off"
msgstr "kapalı"
#: src/lib/menu.sh:234
msgid "home folder"
msgstr "ev klasörü"
#: src/lib/menu.sh:237
msgid "Documents"
msgstr "Belgeler"
#: src/lib/menu.sh:238
msgid "Downloads"
msgstr "İndirilenler"
#: src/lib/menu.sh:243
msgid "not found"
msgstr "bulunamadı"
#: src/lib/menu.sh:246
#, sh-printf-format
msgid "automatic (%d %%)"
msgstr "otomatik (%%%d)"
#: src/lib/menu.sh:248
msgid "classic"
msgstr "klasik"
#: src/lib/menu.sh:249
msgid "GFX"
msgstr "GFX"
#: src/lib/menu.sh:250
msgid "always"
msgstr "her zaman"
#: src/lib/menu.sh:252
msgid "automatic"
msgstr "otomatik"
#: src/lib/menu.sh:261
msgid ""
"The apps only see the files on the server. Pick a folder to open and save "
"files of this computer in them."
msgstr ""
"Uygulamalar yalnızca sunucudaki dosyaları görür. Bu bilgisayarın dosyalarını "
"açıp kaydedebilmeleri için bir klasör seçin."
#: src/lib/menu.sh:262
msgid ""
"The apps can open and save files in this folder. It shows up as a drive in "
"them."
msgstr ""
"Uygulamalar bu klasörde dosya açıp kaydedebilir. Klasör onlarda bir sürücü "
"olarak görünür."
#: src/lib/menu.sh:263
msgid ""
"Offers the apps under \"Open with\" for their file types, for example Word "
"for .docx. The folder of the file is shared with the app while it is open."
msgstr ""
"Uygulamaları dosya türleri için \"Birlikte aç\" menüsünde sunar, "
"örneğin .docx için Word. Dosya açık olduğu sürece klasörü paylaşılır."
#: src/lib/menu.sh:264
msgid ""
"How big the apps are drawn. Automatic follows the scale of your desktop, so "
"they are sharp and the right size."
msgstr ""
"Uygulamaların ne kadar büyük çizileceği. Otomatik, masaüstünüzün ölçeğini "
"izler; böylece keskin ve doğru boyutta olurlar."
#: src/lib/menu.sh:265
msgid "Plays the sound of the apps here, for calls and videos."
msgstr "Uygulamaların sesini burada çalar; aramalar ve videolar için."
#: src/lib/menu.sh:266
msgid ""
"Paints every window right. But some servers end the session right after "
"signing in, then go back to GFX."
msgstr "Her pencereyi doğru çizer. Ama bazı sunucular oturum açar açmaz oturumu kapatır, o zaman GFX'e geri dönün."
#: src/lib/menu.sh:267
msgid ""
"Works with every server. FreeRDP may not repaint a window after it was "
"minimized or resized."
msgstr "Her sunucuyla çalışır. FreeRDP küçültülen ya da boyutu değişen bir pencereyi yeniden çizmeyebilir."
#: src/lib/menu.sh:268
msgid ""
"Always waits before the second sign-in. For when the second prompt does not "
"come with automatic."
msgstr ""
"İkinci oturum açmadan önce hep bekler. Otomatikte ikinci istek gelmiyorsa "
"kullanın."
#: src/lib/menu.sh:269
msgid "Never waits. Right when your company has no MFA."
msgstr "Hiç beklemez. Şirketiniz MFA kullanmıyorsa doğru seçim."
#: src/lib/menu.sh:270
msgid ""
"Many servers with MFA need a second sign-in, and its prompt only comes after "
"a short wait. Automatic waits when the first sign-in took a while."
msgstr ""
"MFA kullanan birçok sunucu ikinci bir oturum açma ister ve isteği ancak kısa "
"bir beklemeden sonra gelir. Otomatik, ilk oturum açma biraz sürdüyse bekler."
#: src/lib/menu.sh:273
msgid ""
"Loads the list of apps again once a day, so new apps show up by themselves."
msgstr ""
"Uygulama listesini günde bir kez yeniden yükler; böylece yeni uygulamalar "
"kendiliğinden görünür."
#: src/lib/menu.sh:275 src/lib/menu.sh:330
msgid "Needs a systemd user session."
msgstr "Bir systemd kullanıcı oturumu gerekir."
#: src/lib/menu.sh:322 src/lib/menu.sh:325
msgid "Could not save the setting."
msgstr "Ayar kaydedilemedi."
#: src/lib/menu.sh:371 src/lib/menu.sh:609
msgid "Settings"
msgstr "Ayarlar"
#: src/lib/menu.sh:372
msgid "↑↓ select ←→ or Space: change q: back"
msgstr "↑↓ seç ←→ ya da Boşluk: değiştir q: geri"
#: src/lib/menu.sh:373
msgid "ON"
msgstr "AÇIK"
#: src/lib/menu.sh:374
msgid "OFF"
msgstr "KAPALI"
#: src/lib/menu.sh:456 src/lib/menu.sh:608
msgid "Workspaces"
msgstr "Çalışma alanları"
#. TRANSLATORS: keep the letters, they are the keys.
#: src/lib/menu.sh:458
msgid ""
"↑↓ select r: refresh s: sign in again d: remove a: add q: back"
msgstr ""
"↑↓ seç r: yenile s: yeniden oturum aç d: kaldır a: ekle q: geri"
#: src/lib/menu.sh:459
msgid "No workspace yet. Press a to add one."
msgstr "Henüz çalışma alanı yok. Eklemek için a tuşuna basın."
#: src/lib/menu.sh:484
#, sh-printf-format
msgid "refreshed %s"
msgstr "%s yenilendi"
#: src/lib/menu.sh:512
#, sh-printf-format
msgid "Remove \"%s\" and its apps?"
msgstr "\"%s\" ve uygulamaları kaldırılsın mı?"
#: src/lib/menu.sh:536 src/lib/menu.sh:607
msgid "Start an app"
msgstr "Bir uygulama başlat"
#: src/lib/menu.sh:537
msgid "↑↓ select Enter: start q: back"
msgstr "↑↓ seç Enter: başlat q: geri"
#: src/lib/menu.sh:538
msgid "No apps yet. Add a workspace first."
msgstr "Henüz uygulama yok. Önce bir çalışma alanı ekleyin."
#: src/lib/menu.sh:579
#, sh-printf-format
msgid "%s is starting. It opens in a window of its own."
msgstr "%s başlatılıyor. Kendi penceresinde açılır."
#: src/lib/menu.sh:606
msgid "Add a workspace"
msgstr "Çalışma alanı ekle"
#: src/lib/menu.sh:610
msgid "Quit"
msgstr "Çık"
#: src/lib/workspace.sh:163
msgid "The server did not accept the user name or password."
msgstr "Sunucu kullanıcı adını ya da parolayı kabul etmedi."
#: src/lib/workspace.sh:164
#, sh-printf-format
msgid "Could not reach the server: %s"
msgstr "Sunucuya ulaşılamadı: %s"
#: src/lib/workspace.sh:165
#, sh-printf-format
msgid "There is no RD Web feed at %s."
msgstr "%s adresinde RD Web akışı yok."
#: src/lib/workspace.sh:166
#, sh-printf-format
msgid "Could not load the apps: %s"
msgstr "Uygulamalar yüklenemedi: %s"
#: src/lib/workspace.sh:282
#, sh-printf-format
msgid "Remote desktop of %s"
msgstr "%s uzak masaüstü"
#: src/lib/workspace.sh:284
#, sh-printf-format
msgid "%s from %s"
msgstr "%s (%s)"
#: src/lib/menu.sh
msgid "Apps"
msgstr "Uygulamalar"
#: src/lib/menu.sh
msgid "Share a folder"
msgstr "Klasör paylaş"
#: src/lib/menu.sh
msgid "Open files with the apps"
msgstr "Dosyaları uygulamalarla aç"
#: src/lib/menu.sh
msgid "Size"
msgstr "Boyut"
#: src/lib/menu.sh
msgid "Sound"
msgstr "Ses"
#: src/lib/menu.sh
msgid "Graphics"
msgstr "Grafik"
#: src/lib/menu.sh
msgid "Signing in"
msgstr "Oturum açma"
#: src/lib/menu.sh
msgid "Wait for the second MFA prompt"
msgstr "İkinci MFA isteğini bekle"
#: src/lib/menu.sh
msgid "Refresh the apps every day"
msgstr "Uygulamaları her gün yenile"
+559
View File
@@ -0,0 +1,559 @@
# Ukrainian translation for rdfeed.
# Copyright (C) 2026 Felitendo
# This file is distributed under the same license as rdfeed.
#
msgid ""
msgstr ""
"Project-Id-Version: rdfeed 1.0.0\n"
"Report-Msgid-Bugs-To: https://github.com/LoonixTools/rdfeed/issues\n"
"POT-Creation-Date: 2026-10-02 10:29+0200\n"
"PO-Revision-Date: 2026-10-02 09:30+0200\n"
"Last-Translator: Felitendo\n"
"Language-Team: Ukrainian\n"
"Language: uk\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: src/rdfeed:79 src/rdfeed:103
#, sh-printf-format
msgid "Could not write to %s."
msgstr "Не вдалося записати в %s."
#: src/rdfeed:82
#, sh-printf-format
msgid "Signing in to %s…"
msgstr "Вхід на %s…"
#: src/rdfeed:100
msgid ""
"The password could not be saved in the keyring, so each app asks for it."
msgstr ""
"Не вдалося зберегти пароль у в'язці ключів, тож кожен застосунок питатиме "
"його."
#: src/rdfeed:111
#, sh-printf-format
msgid "1 app from %s is now in your app menu."
msgstr "1 застосунок із %s тепер у меню застосунків."
#: src/rdfeed:113
#, sh-printf-format
msgid "%d apps from %s are now in your app menu."
msgstr "Застосунків: %d (із %s). Тепер вони в меню застосунків."
#: src/rdfeed:129
msgid ""
"Your IT department has the address. A work e-mail address often works too."
msgstr ""
"Адресу має ваш ІТ-відділ. Часто підходить і робоча адреса електронної пошти."
#: src/rdfeed:130
msgid "Feed address, server or e-mail:"
msgstr "Адреса фіда, сервер або e-mail:"
#: src/rdfeed:137
#, sh-printf-format
msgid "No feed is registered for %s. Ask your IT department for the address."
msgstr "Для %s не зареєстровано фід. Запитайте адресу в ІТ-відділі."
#: src/rdfeed:142 src/rdfeed:156
msgid "User name (DOMAIN\\name or name@domain):"
msgstr "Ім'я користувача (ДОМЕН\\ім'я або ім'я@домен):"
#: src/rdfeed:146 src/rdfeed:159
msgid "Password:"
msgstr "Пароль:"
#: src/rdfeed:178 src/rdfeed:236
#, sh-printf-format
msgid "No workspace yet. Add one with `%s add`."
msgstr "Робочих просторів ще немає. Додайте його командою `%s add`."
#: src/rdfeed:187 src/lib/launch.sh:220
#, sh-printf-format
msgid "No password saved for %s. Sign in again in rdfeed."
msgstr "Для %s не збережено пароль. Увійдіть знову в rdfeed."
#: src/rdfeed:191 src/lib/launch.sh:149
#, sh-printf-format
msgid "Password for %s:"
msgstr "Пароль для %s:"
#: src/rdfeed:195
#, sh-printf-format
msgid "Refreshing %s…"
msgstr "Оновлення %s…"
#: src/rdfeed:205
msgid "The password no longer works. Sign in again in rdfeed."
msgstr "Пароль більше не підходить. Увійдіть знову в rdfeed."
#: src/rdfeed:214
#, sh-printf-format
msgid "%s: %d apps"
msgstr "%s: застосунків: %d"
#: src/rdfeed:222
#, sh-printf-format
msgid "Which workspace? See `%s list` for the names."
msgstr "Який робочий простір? Назви показує `%s list`."
#: src/rdfeed:229
#, sh-printf-format
msgid "%s and its apps are removed."
msgstr "Видалено: %s разом із застосунками."
#: src/rdfeed:253
#, sh-printf-format
msgid "Which app? See `%s list`."
msgstr "Який застосунок? Дивіться `%s list`."
#: src/rdfeed:268 src/lib/launch.sh:199
#, sh-printf-format
msgid "There is no app %s. See `%s list`."
msgstr "Застосунку %s немає. Дивіться `%s list`."
#: src/rdfeed:270
#, sh-printf-format
msgid "Several workspaces have %s. Write it as WORKSPACE/APP, see `%s list`."
msgstr ""
"%s є в кількох робочих просторах. Вкажіть у вигляді ПРОСТІР/ЗАСТОСУНОК, "
"дивіться `%s list`."
#: src/rdfeed:278
msgid "Usage: rdfeed [command]"
msgstr "Використання: rdfeed [команда]"
#: src/rdfeed:280
msgid "Commands:"
msgstr "Команди:"
#: src/rdfeed:281
msgid "Add the workspace of your company"
msgstr "Додати робочий простір вашої компанії"
#: src/rdfeed:282
msgid "Load the apps again"
msgstr "Завантажити застосунки знову"
#: src/rdfeed:283
msgid "List the workspaces and their apps"
msgstr "Показати робочі простори та їхні застосунки"
#: src/rdfeed:284
msgid "Start an app, or open a file with it"
msgstr "Запустити застосунок або відкрити в ньому файл"
#: src/rdfeed:285
msgid "Remove a workspace and its apps"
msgstr "Видалити робочий простір і його застосунки"
#: src/rdfeed:286
msgid "Show this help"
msgstr "Показати цю довідку"
#: src/rdfeed:287
msgid "Show the version"
msgstr "Показати версію"
#: src/rdfeed:289
msgid "Without a command an interactive menu is shown."
msgstr "Без команди відкривається інтерактивне меню."
#: src/rdfeed:313
#, sh-printf-format
msgid "Unknown command: %s"
msgstr "Невідома команда: %s"
#: src/lib/common.sh:174 src/lib/menu.sh:251
msgid "never"
msgstr "ніколи"
#: src/lib/common.sh:184
msgid "just now"
msgstr "щойно"
#: src/lib/common.sh:186
msgid "1 minute ago"
msgstr "1 хвилину тому"
#: src/lib/common.sh:189
#, sh-printf-format
msgid "%d minutes ago"
msgstr "%d хв тому"
#: src/lib/common.sh:191
msgid "1 hour ago"
msgstr "1 годину тому"
#: src/lib/common.sh:194
#, sh-printf-format
msgid "%d hours ago"
msgstr "%d год тому"
#: src/lib/common.sh:196
msgid "1 day ago"
msgstr "1 день тому"
#: src/lib/common.sh:199
#, sh-printf-format
msgid "%d days ago"
msgstr "%d дн. тому"
#: src/lib/launch.sh:169
msgid ""
"Your password has expired. Change it on Windows or on the RD Web page, then "
"sign in again in rdfeed."
msgstr ""
"Термін дії пароля минув. Змініть його у Windows або на сторінці RD Web, "
"потім знову увійдіть у rdfeed."
#: src/lib/launch.sh:171
msgid "Your account is locked. Ask your IT department."
msgstr "Обліковий запис заблоковано. Зверніться до ІТ-відділу."
#: src/lib/launch.sh:173
msgid "The server did not accept the password. Sign in again in rdfeed."
msgstr "Сервер не прийняв пароль. Увійдіть знову в rdfeed."
#: src/lib/launch.sh:175
msgid "Could not reach the server. Check the internet connection."
msgstr "Не вдалося зв'язатися із сервером. Перевірте підключення до інтернету."
#: src/lib/launch.sh:178
#, sh-printf-format
msgid "FreeRDP stopped: %s"
msgstr "FreeRDP зупинився: %s"
#: src/lib/launch.sh:205 src/lib/launch.sh:206
#, sh-printf-format
msgid "File not found: %s"
msgstr "Файл не знайдено: %s"
#: src/lib/launch.sh:212 src/lib/launch.sh:213
msgid ""
"FreeRDP 3 is missing. Install it to start the apps (the package is called "
"freerdp)."
msgstr ""
"Немає FreeRDP 3. Установіть його, щоб запускати застосунки (пакет "
"називається freerdp)."
#: src/lib/launch.sh:277
#, sh-printf-format
msgid "Opening %s in %s"
msgstr "Відкриття %s у %s"
#: src/lib/launch.sh:278
#, sh-printf-format
msgid "If it does not open by itself, open it in the app from the drive %s."
msgstr "Якщо файл не відкриється сам, відкрийте його в застосунку з диска %s."
#: src/lib/launch.sh:280
#, sh-printf-format
msgid "Starting %s"
msgstr "Запуск: %s"
#: src/lib/launch.sh:281
msgid ""
"If your company uses MFA, confirm the sign-in in your authenticator app."
msgstr ""
"Якщо ваша компанія використовує MFA, підтвердьте вхід у застосунку "
"автентифікації."
#: src/lib/launch.sh:290
msgid "A second sign-in is coming"
msgstr "Буде другий вхід"
#: src/lib/launch.sh:291
#, sh-printf-format
msgid ""
"Confirm the second prompt in your authenticator app too. It comes in about "
"%d seconds."
msgstr ""
"Підтвердьте і другий запит у застосунку автентифікації. Він надійде "
"приблизно за %d с."
#: src/lib/launch.sh:301
#, sh-printf-format
msgid "%s could not start"
msgstr "Не вдалося запустити: %s"
#: src/lib/launch.sh:303
#, sh-printf-format
msgid "Log: %s"
msgstr "Журнал: %s"
#. TRANSLATORS: the letter after "[" is the key for yes. y works too.
#: src/lib/menu.sh:93
msgid "[y/N]"
msgstr "[т/Н]"
#: src/lib/menu.sh:163
msgid "1 app"
msgstr "1 застосунок"
#: src/lib/menu.sh:165
#, sh-printf-format
msgid "%d apps"
msgstr "застосунків: %d"
#: src/lib/menu.sh:190
msgid "No workspace yet. Add the one of your company with [1]."
msgstr "Робочих просторів ще немає. Додайте простір вашої компанії через [1]."
#: src/lib/menu.sh:233
msgid "off"
msgstr "вимк."
#: src/lib/menu.sh:234
msgid "home folder"
msgstr "домашня тека"
#: src/lib/menu.sh:237
msgid "Documents"
msgstr "Документи"
#: src/lib/menu.sh:238
msgid "Downloads"
msgstr "Завантаження"
#: src/lib/menu.sh:243
msgid "not found"
msgstr "не знайдено"
#: src/lib/menu.sh:246
#, sh-printf-format
msgid "automatic (%d %%)"
msgstr "автоматично (%d %%)"
#: src/lib/menu.sh:248
msgid "classic"
msgstr "класичний"
#: src/lib/menu.sh:249
msgid "GFX"
msgstr "GFX"
#: src/lib/menu.sh:250
msgid "always"
msgstr "завжди"
#: src/lib/menu.sh:252
msgid "automatic"
msgstr "автоматично"
#: src/lib/menu.sh:261
msgid ""
"The apps only see the files on the server. Pick a folder to open and save "
"files of this computer in them."
msgstr ""
"Застосунки бачать лише файли на сервері. Виберіть теку, щоб вони могли "
"відкривати й зберігати файли цього комп'ютера."
#: src/lib/menu.sh:262
msgid ""
"The apps can open and save files in this folder. It shows up as a drive in "
"them."
msgstr ""
"Застосунки можуть відкривати й зберігати файли в цій теці. У них вона видна "
"як диск."
#: src/lib/menu.sh:263
msgid ""
"Offers the apps under \"Open with\" for their file types, for example Word "
"for .docx. The folder of the file is shared with the app while it is open."
msgstr ""
"Пропонує застосунки в «Відкрити за допомогою» для їхніх типів файлів, "
"наприклад Word для .docx. Тека файлу відкрита для сервера, доки файл "
"відкритий."
#: src/lib/menu.sh:264
msgid ""
"How big the apps are drawn. Automatic follows the scale of your desktop, so "
"they are sharp and the right size."
msgstr ""
"Наскільки великими малюються застосунки. «Автоматично» стежить за масштабом "
"стільниці, тож вони чіткі й потрібного розміру."
#: src/lib/menu.sh:265
msgid "Plays the sound of the apps here, for calls and videos."
msgstr "Відтворює звук застосунків тут, для дзвінків і відео."
#: src/lib/menu.sh:266
msgid ""
"Paints every window right. But some servers end the session right after "
"signing in, then go back to GFX."
msgstr "Правильно малює кожне вікно. Але деякі сервери обривають сеанс одразу після входу, тоді поверніться до GFX."
#: src/lib/menu.sh:267
msgid ""
"Works with every server. FreeRDP may not repaint a window after it was "
"minimized or resized."
msgstr "Працює з будь-яким сервером. FreeRDP може не перемалювати вікно після згортання чи зміни розміру."
#: src/lib/menu.sh:268
msgid ""
"Always waits before the second sign-in. For when the second prompt does not "
"come with automatic."
msgstr ""
"Завжди чекає перед другим входом. Якщо з «автоматично» другий запит не "
"надходить."
#: src/lib/menu.sh:269
msgid "Never waits. Right when your company has no MFA."
msgstr "Ніколи не чекає. Підходить, якщо ваша компанія не використовує MFA."
#: src/lib/menu.sh:270
msgid ""
"Many servers with MFA need a second sign-in, and its prompt only comes after "
"a short wait. Automatic waits when the first sign-in took a while."
msgstr ""
"Багатьом серверам з MFA потрібен другий вхід, і його запит надходить лише "
"після короткої паузи. «Автоматично» чекає, якщо перший вхід тривав якийсь "
"час."
#: src/lib/menu.sh:273
msgid ""
"Loads the list of apps again once a day, so new apps show up by themselves."
msgstr ""
"Раз на день заново завантажує список застосунків, тож нові з'являються самі."
#: src/lib/menu.sh:275 src/lib/menu.sh:330
msgid "Needs a systemd user session."
msgstr "Потрібен користувацький сеанс systemd."
#: src/lib/menu.sh:322 src/lib/menu.sh:325
msgid "Could not save the setting."
msgstr "Не вдалося зберегти налаштування."
#: src/lib/menu.sh:371 src/lib/menu.sh:609
msgid "Settings"
msgstr "Налаштування"
#: src/lib/menu.sh:372
msgid "↑↓ select ←→ or Space: change q: back"
msgstr "↑↓ вибір ←→ або Пробіл: змінити q: назад"
#: src/lib/menu.sh:373
msgid "ON"
msgstr "УВІМК"
#: src/lib/menu.sh:374
msgid "OFF"
msgstr "ВИМК"
#: src/lib/menu.sh:456 src/lib/menu.sh:608
msgid "Workspaces"
msgstr "Робочі простори"
#. TRANSLATORS: keep the letters, they are the keys.
#: src/lib/menu.sh:458
msgid ""
"↑↓ select r: refresh s: sign in again d: remove a: add q: back"
msgstr ""
"↑↓ вибір r: оновити s: увійти знову d: видалити a: додати q: назад"
#: src/lib/menu.sh:459
msgid "No workspace yet. Press a to add one."
msgstr "Робочих просторів ще немає. Натисніть a, щоб додати."
#: src/lib/menu.sh:484
#, sh-printf-format
msgid "refreshed %s"
msgstr "оновлено %s"
#: src/lib/menu.sh:512
#, sh-printf-format
msgid "Remove \"%s\" and its apps?"
msgstr "Видалити «%s» разом із застосунками?"
#: src/lib/menu.sh:536 src/lib/menu.sh:607
msgid "Start an app"
msgstr "Запустити застосунок"
#: src/lib/menu.sh:537
msgid "↑↓ select Enter: start q: back"
msgstr "↑↓ вибір Enter: запустити q: назад"
#: src/lib/menu.sh:538
msgid "No apps yet. Add a workspace first."
msgstr "Застосунків ще немає. Спершу додайте робочий простір."
#: src/lib/menu.sh:579
#, sh-printf-format
msgid "%s is starting. It opens in a window of its own."
msgstr "Запуск: %s. Відкриється в окремому вікні."
#: src/lib/menu.sh:606
msgid "Add a workspace"
msgstr "Додати робочий простір"
#: src/lib/menu.sh:610
msgid "Quit"
msgstr "Вийти"
#: src/lib/workspace.sh:163
msgid "The server did not accept the user name or password."
msgstr "Сервер не прийняв ім'я користувача або пароль."
#: src/lib/workspace.sh:164
#, sh-printf-format
msgid "Could not reach the server: %s"
msgstr "Не вдалося зв'язатися із сервером: %s"
#: src/lib/workspace.sh:165
#, sh-printf-format
msgid "There is no RD Web feed at %s."
msgstr "За адресою %s немає фіда RD Web."
#: src/lib/workspace.sh:166
#, sh-printf-format
msgid "Could not load the apps: %s"
msgstr "Не вдалося завантажити застосунки: %s"
#: src/lib/workspace.sh:282
#, sh-printf-format
msgid "Remote desktop of %s"
msgstr "Віддалена стільниця %s"
#: src/lib/workspace.sh:284
#, sh-printf-format
msgid "%s from %s"
msgstr "%s із %s"
#: src/lib/menu.sh
msgid "Apps"
msgstr "Застосунки"
#: src/lib/menu.sh
msgid "Share a folder"
msgstr "Спільна тека"
#: src/lib/menu.sh
msgid "Open files with the apps"
msgstr "Відкривати файли в застосунках"
#: src/lib/menu.sh
msgid "Size"
msgstr "Розмір"
#: src/lib/menu.sh
msgid "Sound"
msgstr "Звук"
#: src/lib/menu.sh
msgid "Graphics"
msgstr "Графіка"
#: src/lib/menu.sh
msgid "Signing in"
msgstr "Вхід"
#: src/lib/menu.sh
msgid "Wait for the second MFA prompt"
msgstr "Чекати на другий запит MFA"
#: src/lib/menu.sh
msgid "Refresh the apps every day"
msgstr "Оновлювати застосунки щодня"
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
#
# Collects every translatable string into po/rdfeed.pot and brings the
# translations up to date with it.
#
# The settings labels and headings live in an array in menu.sh and reach
# gettext at run time, so xgettext cannot see them; they are pulled out here
# first.
set -euo pipefail
cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.."
tmp="$(mktemp -d)"
trap 'rm -rf -- "$tmp"' EXIT
version="$(make -s version)"
# The labels, as calls xgettext understands, pointing back at menu.sh.
awk -F'|' '/^\t"(user|entries|timer)\|/ { sub(/"$/, "", $5); print "rf_msg \"" $5 "\"" }
/^\t"group\|/ { sub(/"$/, "", $2); print "rf_msg \"" $2 "\"" }' src/lib/menu.sh > "$tmp/settings.sh"
xgettext --from-code=UTF-8 --add-comments=TRANSLATORS --package-name=rdfeed --package-version="$version" \
--msgid-bugs-address=https://github.com/LoonixTools/rdfeed/issues \
-L Shell -k --keyword=rf_msg --keyword=rf_msg_into:2 --keyword=rf_msg_in:2 \
-o po/rdfeed.pot src/rdfeed src/lib/*.sh "$tmp/settings.sh"
sed -i "s|$tmp/settings.sh:[0-9]*|src/lib/menu.sh|g" po/rdfeed.pot
sed -i -e '1i # Translation template for rdfeed.\n# Copyright (C) 2026 Felitendo\n# This file is distributed under the same license as rdfeed.' -e '1,4d' \
po/rdfeed.pot
for po in po/*.po; do
[[ -e $po ]] || continue
msgmerge --quiet --update --backup=none --no-fuzzy-matching "$po" po/rdfeed.pot
done
echo "po/rdfeed.pot: $(grep -c '^msgid' po/rdfeed.pot) strings"
+537
View File
@@ -0,0 +1,537 @@
# Simplified Chinese translation for rdfeed.
# Copyright (C) 2026 Felitendo
# This file is distributed under the same license as rdfeed.
#
msgid ""
msgstr ""
"Project-Id-Version: rdfeed 1.0.0\n"
"Report-Msgid-Bugs-To: https://github.com/LoonixTools/rdfeed/issues\n"
"POT-Creation-Date: 2026-10-02 10:29+0200\n"
"PO-Revision-Date: 2026-10-02 09:30+0200\n"
"Last-Translator: Felitendo\n"
"Language-Team: Simplified Chinese\n"
"Language: zh_CN\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: src/rdfeed:79 src/rdfeed:103
#, sh-printf-format
msgid "Could not write to %s."
msgstr "无法写入 %s。"
#: src/rdfeed:82
#, sh-printf-format
msgid "Signing in to %s…"
msgstr "正在登录 %s…"
#: src/rdfeed:100
msgid ""
"The password could not be saved in the keyring, so each app asks for it."
msgstr "无法把密码保存到密钥环,因此每个应用都会询问密码。"
#: src/rdfeed:111
#, sh-printf-format
msgid "1 app from %s is now in your app menu."
msgstr "来自 %s 的 1 个应用已加入应用菜单。"
#: src/rdfeed:113
#, sh-printf-format
msgid "%d apps from %s are now in your app menu."
msgstr "%d 个应用(来自 %s)已加入应用菜单。"
#: src/rdfeed:129
msgid ""
"Your IT department has the address. A work e-mail address often works too."
msgstr "地址可以向你的 IT 部门索取。很多时候工作邮箱地址也可以。"
#: src/rdfeed:130
msgid "Feed address, server or e-mail:"
msgstr "订阅源地址、服务器或邮箱:"
#: src/rdfeed:137
#, sh-printf-format
msgid "No feed is registered for %s. Ask your IT department for the address."
msgstr "%s 没有登记订阅源。请向 IT 部门询问地址。"
#: src/rdfeed:142 src/rdfeed:156
msgid "User name (DOMAIN\\name or name@domain):"
msgstr "用户名(域\\用户名 或 用户名@域):"
#: src/rdfeed:146 src/rdfeed:159
msgid "Password:"
msgstr "密码:"
#: src/rdfeed:178 src/rdfeed:236
#, sh-printf-format
msgid "No workspace yet. Add one with `%s add`."
msgstr "还没有工作区。用 `%s add` 添加一个。"
#: src/rdfeed:187 src/lib/launch.sh:220
#, sh-printf-format
msgid "No password saved for %s. Sign in again in rdfeed."
msgstr "没有保存 %s 的密码。请在 rdfeed 中重新登录。"
#: src/rdfeed:191 src/lib/launch.sh:149
#, sh-printf-format
msgid "Password for %s:"
msgstr "%s 的密码:"
#: src/rdfeed:195
#, sh-printf-format
msgid "Refreshing %s…"
msgstr "正在刷新 %s…"
#: src/rdfeed:205
msgid "The password no longer works. Sign in again in rdfeed."
msgstr "密码已失效。请在 rdfeed 中重新登录。"
#: src/rdfeed:214
#, sh-printf-format
msgid "%s: %d apps"
msgstr "%s:%d 个应用"
#: src/rdfeed:222
#, sh-printf-format
msgid "Which workspace? See `%s list` for the names."
msgstr "哪个工作区?名称见 `%s list`。"
#: src/rdfeed:229
#, sh-printf-format
msgid "%s and its apps are removed."
msgstr "已移除 %s 及其应用。"
#: src/rdfeed:253
#, sh-printf-format
msgid "Which app? See `%s list`."
msgstr "哪个应用?见 `%s list`。"
#: src/rdfeed:268 src/lib/launch.sh:199
#, sh-printf-format
msgid "There is no app %s. See `%s list`."
msgstr "没有应用 %s。见 `%s list`。"
#: src/rdfeed:270
#, sh-printf-format
msgid "Several workspaces have %s. Write it as WORKSPACE/APP, see `%s list`."
msgstr "多个工作区都有 %s。请写成 工作区/应用,见 `%s list`。"
#: src/rdfeed:278
msgid "Usage: rdfeed [command]"
msgstr "用法:rdfeed [命令]"
#: src/rdfeed:280
msgid "Commands:"
msgstr "命令:"
#: src/rdfeed:281
msgid "Add the workspace of your company"
msgstr "添加你公司的工作区"
#: src/rdfeed:282
msgid "Load the apps again"
msgstr "重新加载应用"
#: src/rdfeed:283
msgid "List the workspaces and their apps"
msgstr "列出工作区及其应用"
#: src/rdfeed:284
msgid "Start an app, or open a file with it"
msgstr "启动应用,或用它打开文件"
#: src/rdfeed:285
msgid "Remove a workspace and its apps"
msgstr "移除工作区及其应用"
#: src/rdfeed:286
msgid "Show this help"
msgstr "显示此帮助"
#: src/rdfeed:287
msgid "Show the version"
msgstr "显示版本"
#: src/rdfeed:289
msgid "Without a command an interactive menu is shown."
msgstr "不带命令时显示交互式菜单。"
#: src/rdfeed:313
#, sh-printf-format
msgid "Unknown command: %s"
msgstr "未知命令:%s"
#: src/lib/common.sh:174 src/lib/menu.sh:251
msgid "never"
msgstr "从未"
#: src/lib/common.sh:184
msgid "just now"
msgstr "刚刚"
#: src/lib/common.sh:186
msgid "1 minute ago"
msgstr "1 分钟前"
#: src/lib/common.sh:189
#, sh-printf-format
msgid "%d minutes ago"
msgstr "%d 分钟前"
#: src/lib/common.sh:191
msgid "1 hour ago"
msgstr "1 小时前"
#: src/lib/common.sh:194
#, sh-printf-format
msgid "%d hours ago"
msgstr "%d 小时前"
#: src/lib/common.sh:196
msgid "1 day ago"
msgstr "1 天前"
#: src/lib/common.sh:199
#, sh-printf-format
msgid "%d days ago"
msgstr "%d 天前"
#: src/lib/launch.sh:169
msgid ""
"Your password has expired. Change it on Windows or on the RD Web page, then "
"sign in again in rdfeed."
msgstr ""
"你的密码已过期。请在 Windows 或 RD Web 页面上修改,然后在 rdfeed 中重新登录。"
#: src/lib/launch.sh:171
msgid "Your account is locked. Ask your IT department."
msgstr "你的账户已被锁定。请联系 IT 部门。"
#: src/lib/launch.sh:173
msgid "The server did not accept the password. Sign in again in rdfeed."
msgstr "服务器没有接受密码。请在 rdfeed 中重新登录。"
#: src/lib/launch.sh:175
msgid "Could not reach the server. Check the internet connection."
msgstr "无法连接服务器。请检查网络连接。"
#: src/lib/launch.sh:178
#, sh-printf-format
msgid "FreeRDP stopped: %s"
msgstr "FreeRDP 已停止:%s"
#: src/lib/launch.sh:205 src/lib/launch.sh:206
#, sh-printf-format
msgid "File not found: %s"
msgstr "找不到文件:%s"
#: src/lib/launch.sh:212 src/lib/launch.sh:213
msgid ""
"FreeRDP 3 is missing. Install it to start the apps (the package is called "
"freerdp)."
msgstr "缺少 FreeRDP 3。请安装它以启动应用(软件包名为 freerdp)。"
#: src/lib/launch.sh:277
#, sh-printf-format
msgid "Opening %s in %s"
msgstr "正在打开 %s(使用 %s)"
#: src/lib/launch.sh:278
#, sh-printf-format
msgid "If it does not open by itself, open it in the app from the drive %s."
msgstr "如果没有自动打开,请在应用中从驱动器 %s 打开它。"
#: src/lib/launch.sh:280
#, sh-printf-format
msgid "Starting %s"
msgstr "正在启动 %s"
#: src/lib/launch.sh:281
msgid ""
"If your company uses MFA, confirm the sign-in in your authenticator app."
msgstr "如果你的公司使用 MFA,请在身份验证器应用中确认登录。"
#: src/lib/launch.sh:290
msgid "A second sign-in is coming"
msgstr "即将进行第二次登录"
#: src/lib/launch.sh:291
#, sh-printf-format
msgid ""
"Confirm the second prompt in your authenticator app too. It comes in about "
"%d seconds."
msgstr "也请在身份验证器应用中确认第二次请求。它大约会在 %d 秒后到达。"
#: src/lib/launch.sh:301
#, sh-printf-format
msgid "%s could not start"
msgstr "无法启动 %s"
#: src/lib/launch.sh:303
#, sh-printf-format
msgid "Log: %s"
msgstr "日志:%s"
#. TRANSLATORS: the letter after "[" is the key for yes. y works too.
#: src/lib/menu.sh:93
msgid "[y/N]"
msgstr "[y/N]"
#: src/lib/menu.sh:163
msgid "1 app"
msgstr "1 个应用"
#: src/lib/menu.sh:165
#, sh-printf-format
msgid "%d apps"
msgstr "%d 个应用"
#: src/lib/menu.sh:190
msgid "No workspace yet. Add the one of your company with [1]."
msgstr "还没有工作区。按 [1] 添加你公司的工作区。"
#: src/lib/menu.sh:233
msgid "off"
msgstr "关"
#: src/lib/menu.sh:234
msgid "home folder"
msgstr "主文件夹"
#: src/lib/menu.sh:237
msgid "Documents"
msgstr "文档"
#: src/lib/menu.sh:238
msgid "Downloads"
msgstr "下载"
#: src/lib/menu.sh:243
msgid "not found"
msgstr "未找到"
#: src/lib/menu.sh:246
#, sh-printf-format
msgid "automatic (%d %%)"
msgstr "自动(%d%%)"
#: src/lib/menu.sh:248
msgid "classic"
msgstr "经典"
#: src/lib/menu.sh:249
msgid "GFX"
msgstr "GFX"
#: src/lib/menu.sh:250
msgid "always"
msgstr "总是"
#: src/lib/menu.sh:252
msgid "automatic"
msgstr "自动"
#: src/lib/menu.sh:261
msgid ""
"The apps only see the files on the server. Pick a folder to open and save "
"files of this computer in them."
msgstr ""
"应用只能看到服务器上的文件。选一个文件夹,它们就能打开和保存这台电脑上的文"
"件。"
#: src/lib/menu.sh:262
msgid ""
"The apps can open and save files in this folder. It shows up as a drive in "
"them."
msgstr "应用可以在这个文件夹里打开和保存文件。它在应用中显示为一个驱动器。"
#: src/lib/menu.sh:263
msgid ""
"Offers the apps under \"Open with\" for their file types, for example Word "
"for .docx. The folder of the file is shared with the app while it is open."
msgstr ""
"在“打开方式”中按文件类型提供这些应用,例如用 Word 打开 .docx。文件打开期间,"
"它所在的文件夹会被共享。"
#: src/lib/menu.sh:264
msgid ""
"How big the apps are drawn. Automatic follows the scale of your desktop, so "
"they are sharp and the right size."
msgstr "应用绘制的大小。“自动”会跟随桌面的缩放,这样应用清晰且大小合适。"
#: src/lib/menu.sh:265
msgid "Plays the sound of the apps here, for calls and videos."
msgstr "在这台电脑上播放应用的声音,用于通话和视频。"
#: src/lib/menu.sh:266
msgid ""
"Paints every window right. But some servers end the session right after "
"signing in, then go back to GFX."
msgstr "能正确绘制每个窗口。但有些服务器会在登录后立即结束会话,这时请改回 GFX。"
#: src/lib/menu.sh:267
msgid ""
"Works with every server. FreeRDP may not repaint a window after it was "
"minimized or resized."
msgstr "适用于所有服务器。窗口最小化或调整大小后,FreeRDP 可能不会重绘。"
#: src/lib/menu.sh:268
msgid ""
"Always waits before the second sign-in. For when the second prompt does not "
"come with automatic."
msgstr "第二次登录前总是等待。适用于选“自动”时第二次请求没有出现的情况。"
#: src/lib/menu.sh:269
msgid "Never waits. Right when your company has no MFA."
msgstr "从不等待。适合公司不使用 MFA 的情况。"
#: src/lib/menu.sh:270
msgid ""
"Many servers with MFA need a second sign-in, and its prompt only comes after "
"a short wait. Automatic waits when the first sign-in took a while."
msgstr ""
"许多使用 MFA 的服务器需要第二次登录,而它的请求要稍等一下才会出现。“自动”会在"
"第一次登录耗时较长时等待。"
#: src/lib/menu.sh:273
msgid ""
"Loads the list of apps again once a day, so new apps show up by themselves."
msgstr "每天重新加载一次应用列表,新应用会自动出现。"
#: src/lib/menu.sh:275 src/lib/menu.sh:330
msgid "Needs a systemd user session."
msgstr "需要 systemd 用户会话。"
#: src/lib/menu.sh:322 src/lib/menu.sh:325
msgid "Could not save the setting."
msgstr "无法保存设置。"
#: src/lib/menu.sh:371 src/lib/menu.sh:609
msgid "Settings"
msgstr "设置"
#: src/lib/menu.sh:372
msgid "↑↓ select ←→ or Space: change q: back"
msgstr "↑↓ 选择 ←→ 或空格:更改 q:返回"
#: src/lib/menu.sh:373
msgid "ON"
msgstr "开启"
#: src/lib/menu.sh:374
msgid "OFF"
msgstr "关闭"
#: src/lib/menu.sh:456 src/lib/menu.sh:608
msgid "Workspaces"
msgstr "工作区"
#. TRANSLATORS: keep the letters, they are the keys.
#: src/lib/menu.sh:458
msgid ""
"↑↓ select r: refresh s: sign in again d: remove a: add q: back"
msgstr "↑↓ 选择 r:刷新 s:重新登录 d:移除 a:添加 q:返回"
#: src/lib/menu.sh:459
msgid "No workspace yet. Press a to add one."
msgstr "还没有工作区。按 a 添加一个。"
#: src/lib/menu.sh:484
#, sh-printf-format
msgid "refreshed %s"
msgstr "%s刷新"
#: src/lib/menu.sh:512
#, sh-printf-format
msgid "Remove \"%s\" and its apps?"
msgstr "移除“%s”及其应用?"
#: src/lib/menu.sh:536 src/lib/menu.sh:607
msgid "Start an app"
msgstr "启动应用"
#: src/lib/menu.sh:537
msgid "↑↓ select Enter: start q: back"
msgstr "↑↓ 选择 Enter:启动 q:返回"
#: src/lib/menu.sh:538
msgid "No apps yet. Add a workspace first."
msgstr "还没有应用。请先添加工作区。"
#: src/lib/menu.sh:579
#, sh-printf-format
msgid "%s is starting. It opens in a window of its own."
msgstr "正在启动 %s。它会在单独的窗口中打开。"
#: src/lib/menu.sh:606
msgid "Add a workspace"
msgstr "添加工作区"
#: src/lib/menu.sh:610
msgid "Quit"
msgstr "退出"
#: src/lib/workspace.sh:163
msgid "The server did not accept the user name or password."
msgstr "服务器没有接受用户名或密码。"
#: src/lib/workspace.sh:164
#, sh-printf-format
msgid "Could not reach the server: %s"
msgstr "无法连接服务器:%s"
#: src/lib/workspace.sh:165
#, sh-printf-format
msgid "There is no RD Web feed at %s."
msgstr "%s 没有 RD Web 订阅源。"
#: src/lib/workspace.sh:166
#, sh-printf-format
msgid "Could not load the apps: %s"
msgstr "无法加载应用:%s"
#: src/lib/workspace.sh:282
#, sh-printf-format
msgid "Remote desktop of %s"
msgstr "%s 的远程桌面"
#: src/lib/workspace.sh:284
#, sh-printf-format
msgid "%s from %s"
msgstr "%s(来自 %s)"
#: src/lib/menu.sh
msgid "Apps"
msgstr "应用"
#: src/lib/menu.sh
msgid "Share a folder"
msgstr "共享文件夹"
#: src/lib/menu.sh
msgid "Open files with the apps"
msgstr "用这些应用打开文件"
#: src/lib/menu.sh
msgid "Size"
msgstr "大小"
#: src/lib/menu.sh
msgid "Sound"
msgstr "声音"
#: src/lib/menu.sh
msgid "Graphics"
msgstr "图形"
#: src/lib/menu.sh
msgid "Signing in"
msgstr "登录"
#: src/lib/menu.sh
msgid "Wait for the second MFA prompt"
msgstr "等待第二次 MFA 请求"
#: src/lib/menu.sh
msgid "Refresh the apps every day"
msgstr "每天刷新应用"
+68
View File
@@ -0,0 +1,68 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
<defs>
<!-- the tile, in the style of face-unlock -->
<linearGradient id="wall" x1="0.1" y1="0" x2="0.9" y2="1">
<stop offset="0" stop-color="#0c2f7a"/>
<stop offset="0.55" stop-color="#1663d6"/>
<stop offset="1" stop-color="#1fb8d6"/>
</linearGradient>
<radialGradient id="glow" cx="0.85" cy="0.1" r="0.6">
<stop offset="0" stop-color="#6fa8ff" stop-opacity="0.7"/>
<stop offset="1" stop-color="#6fa8ff" stop-opacity="0"/>
</radialGradient>
<radialGradient id="teal" cx="0.1" cy="0.95" r="0.6">
<stop offset="0" stop-color="#3ee8d0" stop-opacity="0.55"/>
<stop offset="1" stop-color="#3ee8d0" stop-opacity="0"/>
</radialGradient>
<linearGradient id="sheen" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#ffffff" stop-opacity="0.16"/>
<stop offset="0.5" stop-color="#ffffff" stop-opacity="0"/>
</linearGradient>
<!-- the window -->
<linearGradient id="window" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#15161a"/>
<stop offset="1" stop-color="#000000"/>
</linearGradient>
<linearGradient id="glyph" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#ffffff"/>
<stop offset="1" stop-color="#dbe6f5"/>
</linearGradient>
<clipPath id="tile"><rect x="24" y="24" width="464" height="464" rx="112"/></clipPath>
<filter id="tileShadow" x="-10%" y="-10%" width="120%" height="125%">
<feDropShadow dx="0" dy="8" stdDeviation="10" flood-color="#06163a" flood-opacity="0.35"/>
</filter>
<filter id="windowShadow" x="-25%" y="-25%" width="150%" height="160%">
<feDropShadow dx="0" dy="14" stdDeviation="18" flood-color="#000000" flood-opacity="0.45"/>
</filter>
</defs>
<!-- the tile -->
<g filter="url(#tileShadow)">
<rect x="24" y="24" width="464" height="464" rx="112" fill="url(#wall)"/>
</g>
<g clip-path="url(#tile)">
<rect x="24" y="24" width="464" height="464" fill="url(#glow)"/>
<rect x="24" y="24" width="464" height="464" fill="url(#teal)"/>
<rect x="24" y="24" width="464" height="464" fill="url(#sheen)"/>
</g>
<rect x="25" y="25" width="462" height="462" rx="111" fill="none" stroke="#ffffff" stroke-opacity="0.14" stroke-width="2"/>
<!-- a window, with its title bar -->
<rect x="96" y="110" width="320" height="292" rx="56" fill="url(#window)" filter="url(#windowShadow)"/>
<rect x="97" y="111" width="318" height="290" rx="55" fill="none" stroke="#ffffff" stroke-opacity="0.07" stroke-width="2"/>
<circle cx="146" cy="156" r="9" fill="#ffffff" fill-opacity="0.32"/>
<circle cx="176" cy="156" r="9" fill="#ffffff" fill-opacity="0.32"/>
<circle cx="206" cy="156" r="9" fill="#ffffff" fill-opacity="0.32"/>
<!-- the apps in it, and the feed they come from -->
<g fill="url(#glyph)">
<rect x="152" y="196" width="80" height="80" rx="20"/>
<rect x="152" y="294" width="80" height="80" rx="20" fill-opacity="0.9"/>
<rect x="250" y="294" width="80" height="80" rx="20" fill-opacity="0.8"/>
</g>
<g fill="none" stroke="url(#glyph)" stroke-linecap="round" stroke-width="13">
<path d="M262 204 A 64 64 0 0 1 326 268"/>
<path d="M262 233 A 35 35 0 0 1 297 268"/>
</g>
<circle cx="268" cy="262" r="10" fill="url(#glyph)"/>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

+8
View File
@@ -0,0 +1,8 @@
[Unit]
Description=Load the apps of the rdfeed workspaces again
Documentation=man:rdfeed(1)
After=network-online.target
[Service]
Type=oneshot
ExecStart=@BINDIR@/rdfeed refresh --quiet
+11
View File
@@ -0,0 +1,11 @@
[Unit]
Description=Load the apps of the rdfeed workspaces again every day
Documentation=man:rdfeed(1)
[Timer]
OnCalendar=daily
RandomizedDelaySec=1h
Persistent=true
[Install]
WantedBy=timers.target
+617
View File
@@ -0,0 +1,617 @@
#!/usr/bin/env python3
#
# rdfeed-fetch: sign in to an RD Web Access feed and download its apps
#
# The feed is the one Windows subscribes to under "RemoteApp and Desktop
# Connections". Signing in takes NTLM, which curl no longer always has, so it
# is done here with nothing but the Python standard library.
#
# rdfeed-fetch --url URL --user USER [--domain DOMAIN] --out DIR
#
# The password comes on stdin. DIR gets feed.xml, apps.tsv (app, type, title,
# file types), rdp/<app>.rdp and icons/<app>.png. Progress goes to stdout, one
# tab-separated line each:
#
# workspace <name> the name the company gave the feed
# domain <domain> the Windows domain the server signed us in to
# app <app> <type> <title>
# skip <title> <reason>
#
# Exit codes: 0 done, 10 user name or password wrong, 11 server not reached,
# 12 no feed at that address, 2 bad arguments.
#
# Copyright (C) 2026 Felitendo
# SPDX-License-Identifier: GPL-3.0-or-later
import argparse
import base64
import hmac
import html.parser
import http.client
import os
import re
import socket
import ssl
import struct
import sys
import time
import unicodedata
import urllib.parse
import xml.etree.ElementTree as ET
import zlib
EXIT_AUTH = 10
EXIT_NETWORK = 11
EXIT_NOFEED = 12
# WebFeedLogin.aspx hands back a forms ticket in its body, and the Windows
# client returns it as a cookie. The cookie name is not documented and differs
# between versions, so it goes out under every name in use. IIS reads its own.
FEED_AUTH_COOKIES = ("TSWAFeedAuthCookie", ".ASPXAUTH", "TSWAFeedAuth")
PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
# The file types of the usual apps, for feeds that list none. Windows only
# offers "Open with" for the types the administrator entered, and most never
# enter any. Matched as whole words against the program, the title and the
# alias of an app.
KNOWN_TYPES = [
(("winword", "word"), "docx docm doc dotx dotm dot rtf odt"),
(("excel",), "xlsx xlsm xlsb xls xltx xltm csv ods"),
(("powerpnt", "powerpoint"), "pptx pptm ppt potx potm ppsx pps odp"),
(("onenote",), "one"),
(("outlook",), "msg"),
(("visio",), "vsdx vsd"),
(("msaccess", "access"), "accdb mdb"),
(("winproj", "project"), "mpp"),
(("mspub", "publisher"), "pub"),
(("acrobat", "acrord32", "acrord", "pdf24", "foxit"), "pdf"),
(("trueview", "dwgviewr", "autocad", "acad"), "dwg dxf"),
(("notepad",), "txt log"),
]
class FeedError(Exception):
code = EXIT_NOFEED
class AuthError(FeedError):
code = EXIT_AUTH
# ---------------------------------------------------------------- NTLMv2 (MS-NLMP)
def md4(data):
"""hashlib has no MD4 any more on OpenSSL 3, and NTLM needs it."""
def rol(v, s):
v &= 0xFFFFFFFF
return ((v << s) | (v >> (32 - s))) & 0xFFFFFFFF
msg = bytearray(data) + b"\x80"
while len(msg) % 64 != 56:
msg.append(0)
msg += struct.pack("<Q", (8 * len(data)) & 0xFFFFFFFFFFFFFFFF)
a, b, c, d = 0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476
for off in range(0, len(msg), 64):
x = struct.unpack("<16I", msg[off:off + 64])
aa, bb, cc, dd = a, b, c, d
for i in (0, 4, 8, 12):
a = rol(a + ((b & c) | (~b & d)) + x[i], 3)
d = rol(d + ((a & b) | (~a & c)) + x[i + 1], 7)
c = rol(c + ((d & a) | (~d & b)) + x[i + 2], 11)
b = rol(b + ((c & d) | (~c & a)) + x[i + 3], 19)
for i in (0, 1, 2, 3):
a = rol(a + ((b & c) | (b & d) | (c & d)) + x[i] + 0x5A827999, 3)
d = rol(d + ((a & b) | (a & c) | (b & c)) + x[i + 4] + 0x5A827999, 5)
c = rol(c + ((d & a) | (d & b) | (a & b)) + x[i + 8] + 0x5A827999, 9)
b = rol(b + ((c & d) | (c & a) | (d & a)) + x[i + 12] + 0x5A827999, 13)
for i in (0, 2, 1, 3):
a = rol(a + (b ^ c ^ d) + x[i] + 0x6ED9EBA1, 3)
d = rol(d + (a ^ b ^ c) + x[i + 8] + 0x6ED9EBA1, 9)
c = rol(c + (d ^ a ^ b) + x[i + 4] + 0x6ED9EBA1, 11)
b = rol(b + (c ^ d ^ a) + x[i + 12] + 0x6ED9EBA1, 15)
a, b = (a + aa) & 0xFFFFFFFF, (b + bb) & 0xFFFFFFFF
c, d = (c + cc) & 0xFFFFFFFF, (d + dd) & 0xFFFFFFFF
return struct.pack("<4I", a, b, c, d)
# UNICODE | REQUEST_TARGET | NTLM | ALWAYS_SIGN | EXTENDED_SESSIONSECURITY |
# TARGET_INFO | VERSION | 128 | 56
NTLM_FLAGS = (0x00000001 | 0x00000004 | 0x00000200 | 0x00008000 | 0x00080000
| 0x00800000 | 0x02000000 | 0x20000000 | 0x80000000)
NTLM_VERSION = bytes([10, 0, 0x61, 0x4A, 0, 0, 0, 15])
def ntlm_negotiate():
return (b"NTLMSSP\x00" + struct.pack("<II", 1, NTLM_FLAGS | 0x00000002)
+ struct.pack("<HHI", 0, 0, 40) * 2 + NTLM_VERSION)
def ntlm_av_pairs(info):
pairs, i = {}, 0
while i + 4 <= len(info):
av_id, length = struct.unpack("<HH", info[i:i + 4])
i += 4
if av_id == 0:
break
pairs[av_id] = info[i:i + length]
i += length
return pairs
def ntowfv2(user, domain, password):
return hmac.new(md4(password.encode("utf-16-le")),
(user.upper() + domain).encode("utf-16-le"), "md5").digest()
def ntlmv2_response(key, server_challenge, client_challenge, timestamp, target_info):
temp = (b"\x01\x01" + b"\x00" * 6 + timestamp + client_challenge + b"\x00" * 4
+ target_info + b"\x00" * 4)
return hmac.new(key, server_challenge + temp, "md5").digest() + temp
def ntlm_authenticate(challenge, user, domain, password):
"""The AUTHENTICATE message. domain None takes the server's domain."""
if challenge[:8] != b"NTLMSSP\x00" or struct.unpack("<I", challenge[8:12])[0] != 2:
raise FeedError("invalid NTLM challenge")
server_flags = struct.unpack("<I", challenge[20:24])[0]
server_challenge = challenge[24:32]
info_len, _, info_off = struct.unpack("<HHI", challenge[40:48])
target_info = challenge[info_off:info_off + info_len]
av = ntlm_av_pairs(target_info)
if domain is None:
domain = av.get(2, b"").decode("utf-16-le")
key = ntowfv2(user, domain, password)
client_challenge = os.urandom(8)
timestamp = av.get(7) or struct.pack("<Q", int((time.time() + 11644473600) * 10_000_000))
nt_response = ntlmv2_response(key, server_challenge, client_challenge, timestamp, target_info)
# With a server timestamp the LM response is left empty (MS-NLMP 3.1.5.1.2).
if 7 in av:
lm_response = b"\x00" * 24
else:
lm_response = hmac.new(key, server_challenge + client_challenge, "md5").digest() + client_challenge
workstation = socket.gethostname().split(".")[0].upper()
fields = [lm_response, nt_response, domain.encode("utf-16-le"), user.encode("utf-16-le"),
workstation.encode("utf-16-le"), b""]
offset, secbufs, payload = 72, b"", b""
for field in fields:
secbufs += struct.pack("<HHI", len(field), len(field), offset)
payload += field
offset += len(field)
message = (b"NTLMSSP\x00" + struct.pack("<I", 3) + secbufs
+ struct.pack("<I", server_flags & NTLM_FLAGS) + NTLM_VERSION + payload)
return message, domain
# ---------------------------------------------------------------- HTTP
class HiddenInputs(html.parser.HTMLParser):
def __init__(self):
super().__init__()
self.fields, self.action = {}, None
def handle_starttag(self, tag, attrs):
a = dict(attrs)
if tag == "form" and self.action is None:
self.action = a.get("action")
elif tag == "input" and (a.get("type") or "").lower() == "hidden" and a.get("name"):
self.fields[a["name"]] = a.get("value") or ""
class Session:
"""One keep-alive connection with cookies. NTLM signs in the connection,
not the request, so the handshake has to stay on one socket."""
def __init__(self, url, user, domain, password, timeout=30):
parts = urllib.parse.urlsplit(url)
self.scheme = parts.scheme
self.host = parts.hostname
self.port = parts.port
self.user, self.domain, self.password = user, domain, password
self.detected_domain = domain
self.timeout = timeout
self.cookies = {"TSWAFeatureCheckCookie": "true"}
self.conn = None
self.forms_tried = False
def close(self):
if self.conn:
self.conn.close()
self.conn = None
def _connect(self):
if self.scheme == "http":
return http.client.HTTPConnection(self.host, self.port, timeout=self.timeout)
return http.client.HTTPSConnection(self.host, self.port, timeout=self.timeout,
context=ssl.create_default_context())
def path(self, url, base="/"):
parts = urllib.parse.urlsplit(urllib.parse.urljoin(f"{self.scheme}://{self.host}{base}", url))
if parts.hostname and parts.hostname.lower() != self.host.lower():
raise FeedError(f"redirected to another server: {parts.hostname}")
return urllib.parse.urlunsplit(("", "", parts.path or "/", parts.query, ""))
def send(self, method, path, body=None, headers=None):
hdrs = {"User-Agent": "TSWorkspace/2.0", "Accept": "*/*", "Connection": "keep-alive"}
if self.cookies:
hdrs["Cookie"] = "; ".join(f"{k}={v}" for k, v in self.cookies.items())
hdrs.update(headers or {})
for attempt in range(2):
if self.conn is None:
self.conn = self._connect()
try:
self.conn.request(method, path, body=body, headers=hdrs)
resp = self.conn.getresponse()
data = resp.read()
break
except (http.client.HTTPException, OSError):
self.close()
if attempt:
raise
for name, value in resp.getheaders():
if name.lower() == "set-cookie":
cname, _, rest = value.partition("=")
cvalue = rest.split(";", 1)[0]
if cvalue and "01-jan-1970" not in value.lower():
self.cookies[cname.strip()] = cvalue
else:
self.cookies.pop(cname.strip(), None)
if (resp.getheader("Connection") or "").lower() == "close":
self.close()
return resp, data
def ntlm(self, method, path, body, headers):
auth = {"Authorization": "NTLM " + base64.b64encode(ntlm_negotiate()).decode()}
resp, data = self.send(method, path, body, {**(headers or {}), **auth})
challenge = next((v[5:] for k, v in resp.getheaders()
if k.lower() == "www-authenticate" and v.startswith("NTLM ")), None)
if resp.status != 401 or not challenge:
return resp, data
message, self.detected_domain = ntlm_authenticate(
base64.b64decode(challenge), self.user, self.domain, self.password)
auth = {"Authorization": "NTLM " + base64.b64encode(message).decode()}
return self.send(method, path, body, {**(headers or {}), **auth})
def fetch(self, url, method="GET", body=None, headers=None):
path = original = self.path(url)
token_retry = True
for _ in range(10):
resp, data = self.send(method, path, body, headers)
if resp.status == 401:
offered = " ".join(v.lower() for k, v in resp.getheaders()
if k.lower() == "www-authenticate")
if "ntlm" in offered:
resp, data = self.ntlm(method, path, body, headers)
if resp.status == 401:
raise AuthError("the server did not accept the user name or password")
content_type = (resp.getheader("Content-Type") or "").lower()
if resp.status == 200 and content_type.startswith("application/x-msts-webfeed-login"):
token = data.decode("ascii", "replace").strip()
for name in FEED_AUTH_COOKIES:
self.cookies[name] = token
if path != original and token_retry:
path, token_retry = original, False
continue
location = resp.getheader("Location")
if resp.status in (301, 302, 303, 307, 308) and location:
path = self.path(location, path)
if resp.status not in (307, 308):
method, body, headers = "GET", None, None
continue
return resp, data, path
raise FeedError("too many redirects")
def forms_login(self, login_path):
"""Some servers keep the .rdp files behind the RD Web form login."""
resp, data, path = self.fetch(login_path)
parser = HiddenInputs()
parser.feed(data.decode("utf-8", "replace"))
fields = dict(parser.fields)
user = self.user if "@" in self.user else f"{self.detected_domain}\\{self.user}"
fields.update({"DomainUserName": user, "UserPass": self.password,
"MachineType": "private", "isUtf8": "1"})
fields.setdefault("flags", "0")
action = self.path(parser.action or path, path)
resp, data, path = self.fetch(action, "POST", urllib.parse.urlencode(fields),
{"Content-Type": "application/x-www-form-urlencoded"})
if "login.aspx" in path.lower():
raise AuthError("the RD Web form login failed")
def download(self, url):
resp, data, path = self.fetch(url)
if "login.aspx" in path.lower() and not self.forms_tried:
self.forms_tried = True
self.forms_login(path)
resp, data, path = self.fetch(url)
if resp.status != 200:
raise FeedError(f"HTTP {resp.status}")
return data
# ---------------------------------------------------------------- The feed
def local_tag(el):
return el.tag.rsplit("}", 1)[-1]
def make_slug(text, taken):
# ä → a, é → e: the marks go, the letters stay.
text = "".join(c for c in unicodedata.normalize("NFKD", text) if not unicodedata.combining(c))
base = re.sub(r"[^a-z0-9]+", "-", text.lower()).strip("-")[:40].strip("-") or "app"
slug, n = base, 2
while slug in taken:
slug, n = f"{base}-{n}", n + 1
taken.add(slug)
return slug
def common_suffix(names):
"""The end all aliases share, like "-RemoteApps-CmsRdsh", from a dash on."""
if len(names) < 2:
return ""
suffix = os.path.commonprefix([n[::-1] for n in names])[::-1]
cut = suffix.find("-")
suffix = suffix[cut:] if cut >= 0 else ""
return suffix if all(len(n) > len(suffix) for n in names) else ""
def parse_feed(data):
"""The workspace name and its apps. Raises FeedError if this is no feed."""
try:
root = ET.fromstring(data)
except ET.ParseError:
raise FeedError("the server sent no feed")
if local_tag(root) != "ResourceCollection":
raise FeedError("the server sent no feed")
name = ""
apps = []
for el in root.iter():
tag = local_tag(el)
if tag == "Publisher" and not name:
name = (el.get("Name") or "").strip()
if tag != "Resource":
continue
rtype = el.get("Type") or "RemoteApp"
if rtype not in ("RemoteApp", "Desktop"):
continue
title = (el.get("Title") or "").strip()
alias = el.get("Alias") or title or el.get("ID") or "app"
icons, rdp_url, types = [], None, []
for sub in el.iter():
stag = local_tag(sub)
if stag == "FileExtension" and sub.get("Name"):
ext = sub.get("Name").strip().lstrip(".").lower()
if re.fullmatch(r"[a-z0-9_+-]+", ext) and ext not in types:
types.append(ext)
elif stag.startswith("Icon") and sub.get("FileURL"):
dims = re.findall(r"\d+", sub.get("Dimensions") or stag) or ["0"]
icons.append({"url": sub.get("FileURL"), "type": (sub.get("FileType") or "").lower(),
"size": int(dims[0])})
elif stag == "ResourceFile" and sub.get("URL") and rdp_url is None:
if (sub.get("FileExtension") or ".rdp").lower() == ".rdp":
rdp_url = sub.get("URL")
if rdp_url:
apps.append({"alias": alias, "title": title or alias, "type": rtype,
"rdp": rdp_url, "icons": icons, "types": types})
suffix = common_suffix([a["alias"] for a in apps])
taken = set()
for app in apps:
app["slug"] = make_slug(app["alias"][:len(app["alias"]) - len(suffix)], taken)
return name, apps
def known_types(*names):
"""The file types of a well-known app, from its program, title or alias."""
text = " ".join(names).lower()
for keys, types in KNOWN_TYPES:
if any(re.search(rf"(^|[^a-z0-9]){re.escape(k)}($|[^a-z0-9])", text) for k in keys):
return types.split()
return []
# ---------------------------------------------------------------- .rdp files
def decode_rdp(data):
if data.startswith(b"\xff\xfe") or data.startswith(b"\xfe\xff"):
return data.decode("utf-16")
if b"\x00" in data[:16]:
return data.decode("utf-16-le")
try:
return data.decode("utf-8-sig")
except UnicodeDecodeError:
return data.decode("latin-1")
def rdp_set(text, name, kind, value):
pattern = re.compile(rf"^{re.escape(name)}:[isb]:[^\r\n]*", re.IGNORECASE | re.MULTILINE)
line = f"{name}:{kind}:{value}"
if pattern.search(text):
return pattern.sub(line, text)
return text.rstrip("\r\n") + "\r\n" + line + "\r\n"
def rdp_del(text, name):
return re.sub(rf"^{re.escape(name)}:[isb]:[^\r\n]*\r?\n?", "", text, flags=re.IGNORECASE | re.MULTILINE)
def sanitize_rdp(text):
# One sign-in for the gateway and the session, and never a prompt: started
# from the app menu, there is no terminal to ask in.
text = rdp_set(text, "promptcredentialonce", "i", "1")
text = rdp_set(text, "prompt for credentials on client", "i", "0")
# Drive letters mean nothing here, and FreeRDP reads "*" as every mount,
# camera or USB device of this computer. Sharing a folder is a setting.
for name in ("drivestoredirect", "devicestoredirect", "camerastoredirect", "usbdevicestoredirect"):
text = rdp_del(text, name)
return text
# ---------------------------------------------------------------- Icons
def png_encode(width, height, rows):
"""RGBA rows, top down, as a PNG."""
def chunk(kind, payload):
return (struct.pack(">I", len(payload)) + kind + payload
+ struct.pack(">I", zlib.crc32(kind + payload) & 0xFFFFFFFF))
raw = b"".join(b"\x00" + bytes(row) for row in rows)
return (PNG_SIGNATURE + chunk(b"IHDR", struct.pack(">IIBBBBB", width, height, 8, 6, 0, 0, 0))
+ chunk(b"IDAT", zlib.compress(raw, 9)) + chunk(b"IEND", b""))
def dib_to_png(dib):
"""One bitmap frame of an .ico (BITMAPINFOHEADER, 1 to 32 bits) as a PNG."""
header, width, height2, _, bpp, compression = struct.unpack("<IiiHHI", dib[:20])
height = abs(height2) // 2
if width <= 0 or height <= 0 or compression not in (0, 3) or bpp not in (1, 4, 8, 24, 32):
return None
off = header + (12 if compression == 3 else 0)
palette = []
if bpp <= 8:
count = struct.unpack("<I", dib[32:36])[0] or (1 << bpp)
for i in range(count):
b, g, r = dib[off + 4 * i:off + 4 * i + 3]
palette.append((r, g, b))
off += 4 * count
stride = ((width * bpp + 31) // 32) * 4
pixels = dib[off:off + stride * height]
mask_stride = ((width + 31) // 32) * 4
mask = dib[off + stride * height:off + stride * height + mask_stride * height]
alpha = bpp == 32 and any(pixels[i] for i in range(3, len(pixels), 4))
rows = []
for y in range(height):
src = height - 1 - y
row = pixels[src * stride:(src + 1) * stride]
mrow = mask[src * mask_stride:(src + 1) * mask_stride]
out = bytearray()
for x in range(width):
if bpp == 32:
b, g, r, a = row[4 * x:4 * x + 4]
if not alpha:
a = 255
elif bpp == 24:
b, g, r = row[3 * x:3 * x + 3]
a = 255
else:
bit = x * bpp
index = (row[bit // 8] >> (8 - bpp - bit % 8)) & ((1 << bpp) - 1)
r, g, b = palette[index] if index < len(palette) else (0, 0, 0)
a = 255
if not alpha and x // 8 < len(mrow) and (mrow[x // 8] >> (7 - x % 8)) & 1:
a = 0
out += bytes((r, g, b, a))
rows.append(out)
return png_encode(width, height, rows)
def ico_to_png(data):
"""The biggest frame of an .ico, as a PNG. None if there is none to read."""
if data[:4] != b"\x00\x00\x01\x00":
return None
count = struct.unpack("<H", data[4:6])[0]
entries = []
for i in range(count):
entry = data[6 + 16 * i:22 + 16 * i]
if len(entry) < 16:
break
w, h, _, _, _, bpp, size, offset = struct.unpack("<BBBBHHII", entry)
entries.append(((w or 256) * (h or 256), bpp, size, offset))
for _, _, size, offset in sorted(entries, reverse=True):
frame = data[offset:offset + size]
if frame[:8] == PNG_SIGNATURE:
return frame
try:
png = dib_to_png(frame)
except (struct.error, ValueError, IndexError):
png = None
if png:
return png
return None
def save_icon(session, app, dest):
candidates = [i for i in app["icons"] if i["type"] == "ico"]
candidates += sorted((i for i in app["icons"] if i["type"] != "ico"),
key=lambda i: i["size"], reverse=True)
for icon in candidates:
try:
data = session.download(icon["url"])
except (FeedError, OSError, http.client.HTTPException):
continue
png = data if data[:8] == PNG_SIGNATURE else ico_to_png(data)
if png:
with open(dest, "wb") as f:
f.write(png)
return True
return False
# ---------------------------------------------------------------- Main
def say(*fields):
print("\t".join(str(f).replace("\t", " ").replace("\n", " ") for f in fields), flush=True)
def run(args, password):
session = Session(args.url, args.user, args.domain, password, args.timeout)
resp, data, _ = session.fetch(args.url)
if resp.status != 200:
raise FeedError(f"HTTP {resp.status}")
name, apps = parse_feed(data)
os.makedirs(os.path.join(args.out, "rdp"), exist_ok=True)
os.makedirs(os.path.join(args.out, "icons"), exist_ok=True)
with open(os.path.join(args.out, "feed.xml"), "wb") as f:
f.write(data)
say("workspace", name or session.host)
say("domain", session.detected_domain or "")
done = []
for app in apps:
try:
text = decode_rdp(session.download(app["rdp"]))
if "full address:s:" not in text.lower():
raise FeedError("not an RDP file")
with open(os.path.join(args.out, "rdp", app["slug"] + ".rdp"), "w") as f:
f.write(sanitize_rdp(text))
if not app["types"] and app["type"] == "RemoteApp":
program = re.search(r"^remoteapplicationprogram:s:(.*)$", text, re.IGNORECASE | re.MULTILINE)
app["types"] = known_types(program.group(1) if program else "", app["title"], app["alias"])
save_icon(session, app, os.path.join(args.out, "icons", app["slug"] + ".png"))
except (FeedError, OSError, http.client.HTTPException) as e:
say("skip", app["title"], e)
continue
done.append(app)
say("app", app["slug"], app["type"], app["title"])
with open(os.path.join(args.out, "apps.tsv"), "w") as f:
for app in done:
f.write("\t".join((app["slug"], app["type"], app["title"].replace("\t", " "),
" ".join(app["types"]))) + "\n")
def main(argv):
parser = argparse.ArgumentParser(prog="rdfeed-fetch", description="Download the apps of an RD Web Access feed.")
parser.add_argument("--url", required=True)
parser.add_argument("--user", required=True)
parser.add_argument("--domain", help="Windows domain; without it the server's own is used")
parser.add_argument("--out", required=True, help="directory for the downloaded files")
parser.add_argument("--timeout", type=float, default=30)
args = parser.parse_args(argv)
password = sys.stdin.readline().rstrip("\n")
try:
run(args, password)
except FeedError as e:
print(f"rdfeed-fetch: {e}", file=sys.stderr)
return e.code
except (OSError, http.client.HTTPException) as e:
print(f"rdfeed-fetch: {e}", file=sys.stderr)
return EXIT_NETWORK
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
+146
View File
@@ -0,0 +1,146 @@
/*
* rdfeed-hook: works around two FreeRDP problems, loaded with LD_PRELOAD.
*
* 1. The second MFA prompt. A connection broker sends the client on to the
* server that hosts the session, so FreeRDP opens a second tunnel through
* the RD Gateway right away. With MFA on the gateway (the Azure MFA NPS
* extension), that second sign-in needs a second prompt. When it comes
* less than about 10 seconds after the first one was confirmed, no prompt
* is sent and the gateway waits forever. Windows does not run into this.
* So a new connection to port 443 waits a little first.
*
* RDFEED_GW_WAIT auto (the default), always or never. auto waits only
* when the first sign-in took long enough for somebody to
* confirm a prompt.
* RDFEED_GW_DELAY how long to wait, in seconds (12).
*
* Before waiting it prints "[rdfeed] gateway-delay <seconds>" to stderr,
* so rdfeed can tell the user a second prompt is coming.
*
* 2. The graphics pipeline. FreeRDP 3 always turns GFX on, there is no switch
* to turn it off, and in RemoteApp mode xfreerdp does not repaint a window
* after it was minimized or resized. With RDFEED_NO_GFX set, every attempt
* to turn it on is turned into off, and the classic drawing is used.
*
* It also makes stdout line buffered, so FreeRDP's log reaches rdfeed as it
* happens.
*
* Copyright (C) 2026 Felitendo
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#define _GNU_SOURCE
#include <dlfcn.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
/* A pause this long between two connections means a new tunnel, not the
* second channel of the same one or an IPv6 to IPv4 fallback. */
#define NEW_TUNNEL_SECONDS 2.0
/* A first sign-in that took this long had a person confirming a prompt. */
#define MFA_SECONDS 2.5
static int (*real_connect)(int, const struct sockaddr *, socklen_t);
static int (*real_set_bool)(void *, int, int);
static double first_connect = -1;
static double last_connect = -1;
static double now(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9;
}
__attribute__((constructor)) static void init(void)
{
setvbuf(stdout, NULL, _IOLBF, 0);
}
/* RTLD_NEXT only finds libfreerdp3 when it was loaded globally, as it is in
* xfreerdp. Otherwise ask the library itself. */
static void *freerdp_sym(const char *name)
{
void *sym = dlsym(RTLD_NEXT, name);
if (!sym) {
void *lib = dlopen("libfreerdp3.so.3", RTLD_LAZY | RTLD_NOLOAD);
if (lib)
sym = dlsym(lib, name);
}
return sym;
}
static long gfx_key(void)
{
static long key = -2;
if (key == -2) {
key = -1;
if (getenv("RDFEED_NO_GFX")) {
ssize_t (*by_name)(const char *) =
(ssize_t (*)(const char *))freerdp_sym("freerdp_settings_get_key_for_name");
if (by_name)
key = (long)by_name("FreeRDP_SupportGraphicsPipeline");
}
}
return key;
}
int freerdp_settings_set_bool(void *settings, int id, int val)
{
if (!real_set_bool)
real_set_bool = (int (*)(void *, int, int))freerdp_sym("freerdp_settings_set_bool");
if (!real_set_bool)
return 0;
if (val && id == gfx_key())
val = 0;
return real_set_bool(settings, id, val);
}
static int should_wait(double t)
{
const char *mode = getenv("RDFEED_GW_WAIT");
if (last_connect < 0 || t - last_connect < NEW_TUNNEL_SECONDS)
return 0;
if (mode && strcmp(mode, "never") == 0)
return 0;
if (mode && strcmp(mode, "always") == 0)
return 1;
return t - first_connect >= MFA_SECONDS;
}
int connect(int fd, const struct sockaddr *addr, socklen_t len)
{
if (!real_connect)
real_connect = (int (*)(int, const struct sockaddr *, socklen_t))dlsym(RTLD_NEXT, "connect");
int port = -1;
if (addr && addr->sa_family == AF_INET)
port = ntohs(((const struct sockaddr_in *)addr)->sin_port);
else if (addr && addr->sa_family == AF_INET6)
port = ntohs(((const struct sockaddr_in6 *)addr)->sin6_port);
if (port == 443) {
double t = now();
if (first_connect < 0)
first_connect = t;
if (should_wait(t)) {
const char *env = getenv("RDFEED_GW_DELAY");
unsigned left = env ? (unsigned)atoi(env) : 12;
fprintf(stderr, "[rdfeed] gateway-delay %u\n", left);
fflush(stderr);
while (left)
left = sleep(left);
t = now();
}
last_connect = t;
}
return real_connect(fd, addr, len);
}
+202
View File
@@ -0,0 +1,202 @@
# shellcheck shell=bash
#
# Paths, translations and output helpers.
#
# Everything rdfeed keeps is the user's own: a settings file and one file per
# workspace under ~/.config/rdfeed, the downloaded apps under
# ~/.local/share/rdfeed, and the passwords in the keyring. Nothing needs root.
RF_VERSION="@VERSION@"
RF_NAME="rdfeed"
RF_PRETTY="rdfeed"
RF_LIBDIR="${RF_LIBDIR:-@LIBDIR@}"
RF_LIBEXECDIR="${RF_LIBEXECDIR:-@LIBEXECDIR@}"
RF_LOCALEDIR="${RF_LOCALEDIR:-@LOCALEDIR@}"
RF_FETCH="${RF_FETCH:-$RF_LIBEXECDIR/rdfeed-fetch}"
RF_HOOK="${RF_HOOK:-$RF_LIBEXECDIR/rdfeed-hook.so}"
RF_XDG_CONFIG="${XDG_CONFIG_HOME:-$HOME/.config}"
RF_XDG_DATA="${XDG_DATA_HOME:-$HOME/.local/share}"
RF_XDG_CACHE="${XDG_CACHE_HOME:-$HOME/.cache}"
RF_CONFDIR="${RF_XDG_CONFIG}/${RF_NAME}"
RF_CONFIG="${RF_CONFDIR}/config"
RF_WSDIR="${RF_CONFDIR}/workspaces"
RF_DATADIR="${RF_XDG_DATA}/${RF_NAME}"
RF_CACHEDIR="${RF_XDG_CACHE}/${RF_NAME}"
# For what only lives while an app runs: the .rdp copy that names a file.
RF_RUNDIR="${XDG_RUNTIME_DIR:-$RF_CACHEDIR}/${RF_NAME}"
# Where the app menu looks: the entries, the folder for each workspace (a
# merged menu) and the folder's name and icon.
RF_APPDIR="${RF_XDG_DATA}/applications"
RF_MENUDIR="${RF_XDG_CONFIG}/menus/applications-merged"
RF_DIRDIR="${RF_XDG_DATA}/desktop-directories"
RF_UNIT_TIMER="rdfeed-refresh.timer"
# ---------------------------------------------------------------------------
# Translations
# ---------------------------------------------------------------------------
export TEXTDOMAIN="rdfeed"
export TEXTDOMAINDIR="${RF_LOCALEDIR}"
rf_ui_locale() {
local l="${RF_UI_LOCALE:-}"
if [[ -z $l ]]; then
l="${LC_ALL:-}"
[[ -z $l ]] && l="${LC_MESSAGES:-}"
[[ -z $l ]] && l="${LANG:-}"
fi
# systemd writes /etc/locale.conf and most distributions use it; Debian and
# Ubuntu keep the same LANG= line in /etc/default/locale instead.
if [[ -z $l ]]; then
local f
for f in /etc/locale.conf /etc/default/locale; do
[[ -r $f ]] || continue
l="$(sed -n 's/^LANG=//p' "$f" | tr -d '"' | head -n1)"
[[ -n $l ]] && break
done
fi
printf '%s\n' "${l:-C}"
}
# Every gettext lookup is a fork and the settings screen redraws a screenful of
# labels per keypress, so results are memoized.
declare -A RF_MSG_CACHE=()
RF_MSG_RESULT=''
# rf_msg_into <locale> <msgid>
# Plain lookup with the result in RF_MSG_RESULT and no printf formatting, for
# callers that would otherwise pay a fork per label per frame.
rf_msg_into() {
local locale="$1" msgid="$2" cachekey
cachekey="${locale}"$'\x1f'"${msgid}"
if [[ -n ${RF_MSG_CACHE[$cachekey]+set} ]]; then
RF_MSG_RESULT="${RF_MSG_CACHE[$cachekey]}"
return 0
fi
RF_MSG_RESULT="$(LC_ALL="$locale" LANGUAGE="${locale%%.*}" gettext -- "$msgid" 2>/dev/null)"
[[ -n $RF_MSG_RESULT ]] || RF_MSG_RESULT="$msgid"
RF_MSG_CACHE[$cachekey]="$RF_MSG_RESULT"
return 0
}
# rf_msg_in <locale> <msgid> [printf args...]
rf_msg_in() {
local locale="$1" msgid="$2"
shift 2
rf_msg_into "$locale" "$msgid"
# With no arguments the message is plain text, not a format string. Feeding
# it to printf anyway would turn a literal percent sign in a translation
# into an invalid conversion.
if (( $# == 0 )); then
printf '%s' "$RF_MSG_RESULT"
return
fi
# shellcheck disable=SC2059 # the format string is the translated message
printf -- "$RF_MSG_RESULT" "$@"
}
RF_LOCALE_CACHED=''
# rf_msg <msgid> [printf args...]
rf_msg() {
[[ -n $RF_LOCALE_CACHED ]] || RF_LOCALE_CACHED="$(rf_ui_locale)"
rf_msg_in "$RF_LOCALE_CACHED" "$@"
}
# ---------------------------------------------------------------------------
# Output
# ---------------------------------------------------------------------------
# Decided once, while stdout is still whatever the process was started with:
# testing -t 1 at the point of use is wrong for anything called through $(...),
# which sees a pipe and would conclude nobody is watching.
RF_INTERACTIVE=''
[[ -t 1 ]] && RF_INTERACTIVE=1
if [[ -n $RF_INTERACTIVE && -z ${NO_COLOR:-} ]]; then
RF_C_RESET=$'\033[0m'
RF_C_BOLD=$'\033[1m'
RF_C_DIM=$'\033[2m'
RF_C_BLUE=$'\033[38;2;52;153;255m'
RF_C_GREEN=$'\033[32m'
RF_C_YELLOW=$'\033[33m'
RF_C_RED=$'\033[31m'
else
RF_C_RESET='' RF_C_BOLD='' RF_C_DIM='' RF_C_BLUE=''
RF_C_GREEN='' RF_C_YELLOW='' RF_C_RED=''
fi
# What rf_ok, rf_bad and rf_note printed. The menu redraws straight after an
# action, which wipes the screen, so it shows these again under the new frame
# rather than holding everything up for a key press.
RF_UI_NOTICES=()
rf_say() { printf '%s\n' "$*"; }
rf_head() { printf '\n%s%s%s\n\n' "$RF_C_BOLD$RF_C_BLUE" "$*" "$RF_C_RESET"; }
rf_ok() { RF_UI_NOTICES+=("$RF_C_GREEN✔$RF_C_RESET $*"); printf '%s✔%s %s\n' "$RF_C_GREEN" "$RF_C_RESET" "$*"; }
rf_bad() { RF_UI_NOTICES+=("$RF_C_RED✘$RF_C_RESET $*"); printf '%s✘%s %s\n' "$RF_C_RED" "$RF_C_RESET" "$*" >&2; }
rf_note() { RF_UI_NOTICES+=("$RF_C_DIM•$RF_C_RESET $*"); printf '%s•%s %s\n' "$RF_C_DIM" "$RF_C_RESET" "$*"; }
rf_have() { command -v "$1" > /dev/null 2>&1; }
# rf_notify <summary> <body> [icon]
# A desktop notification, for what happens after the menu or the app menu
# started something and nobody watches a terminal.
rf_notify() {
rf_have notify-send || return 0
notify-send -a "$RF_PRETTY" -i "${3:-rdfeed}" -- "$1" "$2" > /dev/null 2>&1 || true
}
# Human-readable "x minutes ago" for a unix timestamp. 0 or empty yields the
# translated "never".
#
# Written with [[ ]] and a variable for each number on purpose: xgettext reads
# the < of an (( )) as a redirection and loses every string after it.
rf_time_ago() {
local ts="$1" now delta n
if [[ ! $ts =~ ^[0-9]+$ || $ts -eq 0 ]]; then
rf_msg "never"
printf '\n'
return
fi
now="$(date +%s)"
delta=$(( now - ts ))
[[ $delta -lt 0 ]] && delta=0
if [[ $delta -lt 60 ]]; then
rf_msg "just now"
elif [[ $delta -lt 120 ]]; then
rf_msg "1 minute ago"
elif [[ $delta -lt 3600 ]]; then
n=$(( delta / 60 ))
rf_msg "%d minutes ago" "$n"
elif [[ $delta -lt 7200 ]]; then
rf_msg "1 hour ago"
elif [[ $delta -lt 86400 ]]; then
n=$(( delta / 3600 ))
rf_msg "%d hours ago" "$n"
elif [[ $delta -lt 172800 ]]; then
rf_msg "1 day ago"
else
n=$(( delta / 86400 ))
rf_msg "%d days ago" "$n"
fi
printf '\n'
}
+155
View File
@@ -0,0 +1,155 @@
# shellcheck shell=bash
#
# Reading and writing the settings files.
#
# ~/.config/rdfeed/config holds how the apps run (a shared folder, the size,
# the graphics), and ~/.config/rdfeed/workspaces/<id> where a workspace comes
# from and who signs in to it. Neither is meant to be edited by hand: every
# option is in the menu.
#
# Both are parsed rather than sourced. One "Key=Value" per line, '#' comments.
declare -A RF_KV_CACHE=()
# _rf_kv_lookup <file> <Key> [default]
# Result in RF_KV_VALUE. Assigning rather than printing matters on the
# settings screen, which reads every key on every frame: a command
# substitution there is a fork, and forks are the whole cost of a redraw.
RF_KV_VALUE=''
_rf_kv_lookup() {
local file="$1" key="$2" default="${3:-}" val='' line content
RF_KV_VALUE="$default"
[[ -r $file ]] || return 0
if [[ -n ${RF_KV_CACHE[$file]+set} ]]; then
content="${RF_KV_CACHE[$file]}"
else
content="$(< "$file")"
RF_KV_CACHE[$file]="$content"
fi
while IFS= read -r line; do
[[ $line == *"$key"* ]] || continue
[[ $line =~ ^[[:space:]]*"$key"[[:space:]]*=(.*)$ ]] || continue
val="${BASH_REMATCH[1]}"
done <<< "$content"
val="${val#"${val%%[![:space:]]*}"}"
val="${val%"${val##*[![:space:]]}"}"
[[ -n $val ]] && RF_KV_VALUE="$val"
return 0
}
rf_is_true() {
case "${1,,}" in
yes|y|true|1|on|enabled) return 0 ;;
*) return 1 ;;
esac
}
# rf_kv_set <file> <Key> <Value> <header line>
rf_kv_set() {
local file="$1" key="$2" value="$3" header="$4" tmp
if [[ ! -e $file ]]; then
mkdir -p "$(dirname "$file")" || return 1
{
printf '# %s\n' "$header"
printf '#\n'
# shellcheck disable=SC2016 # the backticks are text
printf '# Written by `%s`. Nothing here needs editing by hand:\n' "$RF_NAME"
printf '# every option is in the menu.\n'
} > "$file" || return 1
fi
[[ -w $file ]] || return 1
tmp="$(mktemp "${file}.XXXXXX")" || return 1
chmod --reference="$file" "$tmp" 2>/dev/null || chmod 0644 "$tmp"
if grep -qE "^[[:space:]]*#?[[:space:]]*${key}[[:space:]]*=" "$file"; then
awk -v key="$key" -v value="$value" '
!done && $0 ~ "^[[:space:]]*#?[[:space:]]*" key "[[:space:]]*=" {
print key "=" value; done = 1; next
}
# drop any further occurrences so the file cannot grow duplicates
$0 ~ "^[[:space:]]*" key "[[:space:]]*=" { next }
{ print }
' "$file" > "$tmp" || { rm -f "$tmp"; return 1; }
else
cat "$file" > "$tmp" || { rm -f "$tmp"; return 1; }
printf '%s=%s\n' "$key" "$value" >> "$tmp"
fi
mv -f "$tmp" "$file"
unset 'RF_KV_CACHE[$file]'
}
# ---------------------------------------------------------------------------
# The settings
# ---------------------------------------------------------------------------
rf_config_get() {
_rf_kv_lookup "$RF_CONFIG" "$@"
printf '%s\n' "$RF_KV_VALUE"
}
rf_config_set() {
rf_kv_set "$RF_CONFIG" "$1" "$2" "$RF_PRETTY"
}
# rf_config_load
# Everything starting an app needs, resolved once.
rf_config_load() {
RF_KV_CACHE=()
_rf_kv_lookup "$RF_CONFIG" Share off; CFG_SHARE="$RF_KV_VALUE"
_rf_kv_lookup "$RF_CONFIG" Scale auto; CFG_SCALE="$RF_KV_VALUE"
_rf_kv_lookup "$RF_CONFIG" Graphics gfx; CFG_GRAPHICS="$RF_KV_VALUE"
_rf_kv_lookup "$RF_CONFIG" MfaWait auto; CFG_MFAWAIT="$RF_KV_VALUE"
_rf_kv_lookup "$RF_CONFIG" Sound yes; CFG_SOUND=no; rf_is_true "$RF_KV_VALUE" && CFG_SOUND=yes
return 0
}
# ---------------------------------------------------------------------------
# Workspaces
# ---------------------------------------------------------------------------
rf_ws_file() { printf '%s/%s\n' "$RF_WSDIR" "$1"; }
# rf_ws_ids
# The workspaces, sorted, in RF_WS_IDS.
RF_WS_IDS=()
rf_ws_ids() {
local f
RF_WS_IDS=()
[[ -d $RF_WSDIR ]] || return 0
for f in "$RF_WSDIR"/*; do
[[ -f $f ]] || continue
RF_WS_IDS+=("${f##*/}")
done
}
# rf_ws_load <id>
# Sets WS_URL, WS_NAME, WS_USER, WS_DOMAIN, WS_REFRESHED and WS_ACCOUNT, the
# name the way Windows writes it (DOMAIN\user, or user@domain as typed).
rf_ws_load() {
local file
file="$(rf_ws_file "$1")"
_rf_kv_lookup "$file" Url; WS_URL="$RF_KV_VALUE"
_rf_kv_lookup "$file" Name "$1"; WS_NAME="$RF_KV_VALUE"
_rf_kv_lookup "$file" User; WS_USER="$RF_KV_VALUE"
_rf_kv_lookup "$file" Domain; WS_DOMAIN="$RF_KV_VALUE"
_rf_kv_lookup "$file" Refreshed 0; WS_REFRESHED="$RF_KV_VALUE"
if [[ -n $WS_DOMAIN && $WS_USER != *@* ]]; then
WS_ACCOUNT="$WS_DOMAIN\\$WS_USER"
else
WS_ACCOUNT="$WS_USER"
fi
}
rf_ws_set() {
rf_kv_set "$(rf_ws_file "$1")" "$2" "$3" "$RF_PRETTY workspace"
}
+307
View File
@@ -0,0 +1,307 @@
# shellcheck shell=bash
#
# Starting an app.
#
# Each app runs in xfreerdp in RemoteApp mode: only its own windows show, not
# a whole Windows desktop. The arguments, the password among them, go to
# FreeRDP on stdin (/args-from:stdin), never on its command line.
#
# A file from this computer (an "Open with", or `rdfeed run APP FILE`) gets
# there the way Windows does it: its folder is shared as a drive, and the app
# is started with the path on that drive, \\tsclient\<folder>\<file>.
#
# rdfeed-hook.so goes into FreeRDP with LD_PRELOAD. It waits before the second
# gateway sign-in so a second MFA prompt is sent, and keeps the graphics
# pipeline off, which leaves windows unpainted in RemoteApp mode. See
# src/hook/rdfeed-hook.c.
# How long the hook waits before the second sign-in, in seconds.
RF_GW_DELAY=12
# rf_freerdp
# The FreeRDP 3 client that can show RemoteApps (the X11 one), in RF_FREERDP.
# Arch and Debian call it xfreerdp3, Fedora xfreerdp.
RF_FREERDP=''
rf_freerdp() {
RF_FREERDP=''
if rf_have xfreerdp3; then
RF_FREERDP=xfreerdp3
elif rf_have xfreerdp && [[ $(xfreerdp /version 2>/dev/null) == *" 3."* ]]; then
RF_FREERDP=xfreerdp
fi
[[ -n $RF_FREERDP ]]
}
# rf_kbd_layout
# The Windows id of the keyboard layout in use. Under Wayland, X11 programs
# often see "us" whatever the real layout is, so it is asked of the desktop.
rf_kbd_layout() {
local layout=''
if [[ -r $RF_XDG_CONFIG/kxkbrc ]]; then
layout="$(sed -n 's/^LayoutList=//p' "$RF_XDG_CONFIG/kxkbrc" | head -n1)"
fi
if [[ -z $layout && ${XDG_CURRENT_DESKTOP:-} == *GNOME* ]] && rf_have gsettings; then
layout="$(gsettings get org.gnome.desktop.input-sources sources 2>/dev/null \
| sed -n "s/^[^']*'xkb', '\([a-z]*\).*/\1/p")"
fi
if [[ -z $layout ]] && rf_have localectl; then
layout="$(localectl status 2>/dev/null | sed -n 's/.*X11 Layout: *//p')"
fi
[[ -z $layout ]] && layout="${XKB_DEFAULT_LAYOUT:-}"
layout="${layout%%,*}"
case "${layout,,}" in
de|at) printf '0x00000407\n' ;;
ch) printf '0x00000807\n' ;;
us) printf '0x00000409\n' ;;
gb|uk) printf '0x00000809\n' ;;
fr) printf '0x0000040C\n' ;;
be) printf '0x0000080C\n' ;;
es) printf '0x0000040A\n' ;;
it) printf '0x00000410\n' ;;
nl) printf '0x00000413\n' ;;
pl) printf '0x00000415\n' ;;
pt) printf '0x00000816\n' ;;
br) printf '0x00000416\n' ;;
ru) printf '0x00000419\n' ;;
ua) printf '0x00000422\n' ;;
tr) printf '0x0000041F\n' ;;
jp) printf '0x00000411\n' ;;
kr) printf '0x00000412\n' ;;
se) printf '0x0000041D\n' ;;
no) printf '0x00000414\n' ;;
dk) printf '0x00000406\n' ;;
fi) printf '0x0000040B\n' ;;
cz) printf '0x00000405\n' ;;
sk) printf '0x0000041B\n' ;;
hu) printf '0x0000040E\n' ;;
*) return 1 ;;
esac
}
# rf_auto_scale
# How much the desktop scales X11 programs that scale themselves, in percent,
# in RF_AUTO_SCALE. The apps are then drawn that much bigger on the server, so
# they come out sharp and the right size.
RF_AUTO_SCALE=100
rf_auto_scale() {
local s dpi
RF_AUTO_SCALE=100
s="$(awk -F= '/^\[/ { section = $0 } section == "[Xwayland]" && $1 == "Scale" { print $2 }' \
"$RF_XDG_CONFIG/kwinrc" 2>/dev/null | tail -n1)"
if [[ $s =~ ^[0-9]+(\.[0-9]+)?$ ]]; then
RF_AUTO_SCALE="$(awk -v s="$s" 'BEGIN { printf "%d", s * 100 + 0.5 }')"
elif rf_have xrdb; then
dpi="$(xrdb -query 2>/dev/null | awk '/^Xft\.dpi:/ { print $2 }')"
[[ $dpi =~ ^[0-9]+$ ]] && RF_AUTO_SCALE=$(( (dpi * 100 + 48) / 96 ))
fi
[[ $RF_AUTO_SCALE -lt 100 ]] && RF_AUTO_SCALE=100
[[ $RF_AUTO_SCALE -gt 500 ]] && RF_AUTO_SCALE=500
return 0
}
# rf_share_dir
# The folder the apps may see, from the Share setting. Nothing for off.
rf_share_dir() {
local dir=''
case "$CFG_SHARE" in
home) dir="$HOME" ;;
documents) rf_have xdg-user-dir && dir="$(xdg-user-dir DOCUMENTS)" ;;
downloads) rf_have xdg-user-dir && dir="$(xdg-user-dir DOWNLOAD)" ;;
esac
# xdg-user-dir answers with the home folder for a folder that is not set
# up. Sharing all of it then would be a surprise.
[[ $CFG_SHARE != home && ${dir%/} == "${HOME%/}" ]] && return 1
[[ -n $dir && -d $dir ]] || return 1
printf '%s\n' "$dir"
}
# _rf_drive_name <dir>
# A name for a shared folder, as the app shows it: the folder's own name,
# without what Windows does not allow in one.
_rf_drive_name() {
local name="${1%/}"
name="${name##*/}"
name="${name//[\\\/:*?\"<>|,;]/_}"
printf '%s\n' "${name:-Linux}"
}
# _rf_drive_path <dir>
# The folder as FreeRDP can take it. /drive splits at commas, so a folder with
# one in its path is shared through a link that has none.
_rf_drive_path() {
local dir="$1" link
if [[ $dir != *,* ]]; then
printf '%s\n' "$dir"
return
fi
link="$RF_RUNDIR/links/$(_rf_drive_name "$dir")"
link="${link//,/_}"
mkdir -p "${link%/*}" && ln -sfn "$dir" "$link" && printf '%s\n' "$link"
}
# rf_ask_password <account>
# For when the keyring has none: in the terminal if there is one, else in a
# small window.
rf_ask_password() {
local prompt password
prompt="$(rf_msg "Password for %s:" "$1")"
if [[ -t 0 ]]; then
IFS= read -rsp " $prompt " password || return 1
printf '\n' >&2
elif rf_have kdialog; then
password="$(kdialog --title "$RF_PRETTY" --password "$prompt" 2>/dev/null)" || return 1
elif rf_have zenity; then
password="$(zenity --password --title "$prompt" 2>/dev/null)" || return 1
else
return 1
fi
[[ -n $password ]] || return 1
printf '%s' "$password"
}
# rf_launch_error <log>
# Why FreeRDP gave up, in words, from what it logged.
rf_launch_error() {
local log="$1" last
if grep -qE 'ERRCONNECT_(PASSWORD_EXPIRED|PASSWORD_MUST_CHANGE|PASSWORD_CERTAINLY_EXPIRED)' "$log"; then
rf_msg "Your password has expired. Change it on Windows or on the RD Web page, then sign in again in rdfeed."
elif grep -qE 'ERRCONNECT_ACCOUNT_LOCKED_OUT' "$log"; then
rf_msg "Your account is locked. Ask your IT department."
elif grep -qE 'ERRCONNECT_(LOGON_FAILURE|WRONG_PASSWORD|AUTHENTICATION_FAILED)|STATUS_LOGON_FAILURE' "$log"; then
rf_msg "The server did not accept the password. Sign in again in rdfeed."
elif grep -qE 'ERRCONNECT_(DNS_NAME_NOT_FOUND|DNS_ERROR|CONNECT_FAILED|CONNECT_TRANSPORT_FAILED)' "$log"; then
rf_msg "Could not reach the server. Check the internet connection."
else
last="$(grep '\[ERROR\]' "$log" | tail -n1 | sed 's/^.*\] - \[[^]]*\]: *//')"
rf_msg "FreeRDP stopped: %s" "${last:-?}"
fi
}
# rf_launch <id> <app> [file]
rf_launch() {
local id="$1" slug="$2" file="${3:-}" i title='' type='' icon rdp password log rcfile rc line kbd share
local drive='' remote='' temprdp='' dir name rel
local -a args env
rf_ws_load "$id"
rf_ws_apps_load "$id"
for i in "${!RF_APP_SLUGS[@]}"; do
if [[ ${RF_APP_SLUGS[i]} == "$slug" ]]; then
title="${RF_APP_TITLES[i]}" type="${RF_APP_TYPES[i]}"
fi
done
rdp="$RF_DATADIR/$id/rdp/$slug.rdp"
icon="$RF_DATADIR/$id/icons/$slug.png"
[[ -f $icon ]] || icon=rdfeed
if [[ -z $title || ! -f $rdp ]]; then
rf_bad "$(rf_msg "There is no app %s. See \`%s list\`." "$id/$slug" "$RF_NAME")"
return 1
fi
if [[ -n $file ]]; then
file="$(realpath -e -- "$file" 2>/dev/null)" && [[ -f $file ]] || {
rf_bad "$(rf_msg "File not found: %s" "${3:-}")"
rf_notify "$title" "$(rf_msg "File not found: %s" "${3:-}")" dialog-error
return 1
}
fi
if ! rf_freerdp; then
rf_bad "$(rf_msg "FreeRDP 3 is missing. Install it to start the apps (the package is called freerdp).")"
rf_notify "$title" "$(rf_msg "FreeRDP 3 is missing. Install it to start the apps (the package is called freerdp).")" dialog-error
return 1
fi
password="$(rf_secret_get "$id" "$WS_ACCOUNT")" || password=''
if [[ -z $password ]]; then
password="$(rf_ask_password "$WS_ACCOUNT")" || {
rf_notify "$title" "$(rf_msg "No password saved for %s. Sign in again in rdfeed." "$WS_NAME")" dialog-error
return 1
}
fi
rf_config_load
args=("$rdp" "/u:$WS_USER")
[[ -n $WS_DOMAIN && $WS_USER != *@* ]] && args+=("/d:$WS_DOMAIN")
args+=("/p:$password" /cert:tofu +clipboard)
kbd="$(rf_kbd_layout)" && args+=("/kbd:layout:$kbd")
[[ $CFG_SOUND == yes ]] && args+=(/sound)
if [[ $CFG_SCALE == auto ]]; then
rf_auto_scale
else
RF_AUTO_SCALE="$CFG_SCALE"
fi
[[ $RF_AUTO_SCALE -gt 100 ]] && args+=("/scale-desktop:$RF_AUTO_SCALE")
if share="$(rf_share_dir)"; then
name="$(_rf_drive_name "$share")"
args+=("/drive:$name,$(_rf_drive_path "$share")")
# A file in the shared folder needs no drive of its own.
if [[ -n $file && $file == "${share%/}"/* ]]; then
drive="$name"
rel="${file#"${share%/}"/}"
remote="\\\\tsclient\\$name\\${rel//\//\\}"
fi
fi
if [[ -n $file && -z $remote ]]; then
dir="${file%/*}"
drive="$(_rf_drive_name "$dir")"
args+=("/drive:$drive,$(_rf_drive_path "${dir:-/}")")
remote="\\\\tsclient\\$drive\\${file##*/}"
fi
if [[ -n $remote ]]; then
# The app gets the file as its command line, in a copy of the .rdp.
mkdir -p "$RF_RUNDIR"
temprdp="$(mktemp "$RF_RUNDIR/$id--$slug.XXXXXX.rdp")"
RF_CMDLINE="\"$remote\"" awk '
BEGIN { line = "remoteapplicationcmdline:s:" ENVIRON["RF_CMDLINE"] }
tolower($0) ~ /^remoteapplicationcmdline:s:/ { if (!done) print line "\r"; done = 1; next }
{ print }
END { if (!done) print line "\r" }
' "$rdp" > "$temprdp"
args[0]="$temprdp"
fi
[[ $type == Desktop ]] && args+=(/dynamic-resolution)
env=(WLOG_LEVEL="${WLOG_LEVEL:-INFO}" RDFEED_GW_WAIT="$CFG_MFAWAIT" RDFEED_GW_DELAY="$RF_GW_DELAY")
[[ $CFG_GRAPHICS == gfx ]] || env+=(RDFEED_NO_GFX=1)
[[ -f $RF_HOOK ]] && env+=(LD_PRELOAD="$RF_HOOK${LD_PRELOAD:+:$LD_PRELOAD}")
mkdir -p "$RF_CACHEDIR"
log="$RF_CACHEDIR/$id--$slug.log"
rcfile="$(mktemp "$RF_CACHEDIR/.exit.XXXXXX")"
: > "$log"
if [[ -n $file ]]; then
rf_notify "$(rf_msg "Opening %s in %s" "${file##*/}" "$title")" \
"$(rf_msg "If it does not open by itself, open it in the app from the drive %s." "$drive")" "$icon"
else
rf_notify "$(rf_msg "Starting %s" "$title")" \
"$(rf_msg "If your company uses MFA, confirm the sign-in in your authenticator app.")" "$icon"
fi
{
printf '%s\n' "${args[@]}" | env "${env[@]}" "$RF_FREERDP" /args-from:stdin 2>&1
printf '%s\n' "${PIPESTATUS[1]}" > "$rcfile"
} | while IFS= read -r line; do
printf '%s\n' "$line" >> "$log"
if [[ $line == "[rdfeed] gateway-delay"* ]]; then
rf_notify "$(rf_msg "A second sign-in is coming")" \
"$(rf_msg "Confirm the second prompt in your authenticator app too. It comes in about %d seconds." "$RF_GW_DELAY")" "$icon"
fi
done
rc="$(< "$rcfile")"
rm -f "$rcfile"
[[ -n $temprdp ]] && rm -f "$temprdp"
# Below 128 the session ended the normal way: closed, logged off, or taken
# over by the next app of the same workspace.
if [[ $rc =~ ^[0-9]+$ ]] && (( rc >= 128 )) && ! grep -q ERRCONNECT_CONNECT_CANCELLED "$log"; then
rf_notify "$(rf_msg "%s could not start" "$title")" "$(rf_launch_error "$log")" dialog-error
rf_bad "$(rf_launch_error "$log")"
rf_note "$(rf_msg "Log: %s" "$log")"
return 1
fi
return 0
}
+635
View File
@@ -0,0 +1,635 @@
# shellcheck shell=bash
#
# The interactive front end.
#
# This is the whole configuration interface. There are files behind it, but no
# part of the program ever asks anybody to open one: the workspaces, the apps,
# a settings list.
# Terminal mode.
#
# bash flips the terminal into non-canonical mode for each `read -sn1` and back
# out again in between. That gap matters: in canonical mode DEL is the ERASE
# character, so the line discipline eats it instead of delivering it, and a
# backspace typed while the interface was between reads simply vanishes.
# Holding non-canonical mode for the whole interface removes the gap.
RF_TERM_SAVED=''
rf_ui_term_raw() {
rf_have stty || return 0
[[ -t 0 ]] || return 0
[[ -n $RF_TERM_SAVED ]] && return 0
RF_TERM_SAVED="$(stty -g 2>/dev/null)" || { RF_TERM_SAVED=''; return 0; }
stty -icanon -echo min 1 time 0 2>/dev/null || true
}
rf_ui_term_restore() {
[[ -n $RF_TERM_SAVED ]] || return 0
stty "$RF_TERM_SAVED" 2>/dev/null || true
RF_TERM_SAVED=''
}
# Runs an action with the terminal handed back to normal line mode, so what it
# prompts for (a user name, a password) behaves the way a program expects.
rf_ui_cooked() {
rf_ui_term_restore
"$@"
local rc=$?
rf_ui_term_raw
return $rc
}
# rf_read_key
# One keypress, resolved to a symbolic name. Arrow keys arrive as ESC [ A, so
# the tail of the sequence is consumed here rather than being mistaken for
# three separate presses.
rf_read_key() {
local k rest
IFS= read -rsn1 k || return 1
case "$k" in
$'\e')
if IFS= read -rsn2 -t 0.05 rest; then
case "$rest" in
'[A') printf 'up\n' ;;
'[B') printf 'down\n' ;;
'[C') printf 'right\n' ;;
'[D') printf 'left\n' ;;
*) printf 'escape\n' ;;
esac
else
printf 'escape\n'
fi
;;
''|$'\r') printf 'enter\n' ;;
$'\x7f'|$'\b') printf 'backspace\n' ;;
' ') printf 'space\n' ;;
*) printf '%s\n' "$k" ;;
esac
}
# _rf_ui_take_notices
# The messages the last action left, as lines for under a frame, in
# RF_UI_NOTICE_TEXT. Each is shown once.
RF_UI_NOTICE_TEXT=''
_rf_ui_take_notices() {
local n
RF_UI_NOTICE_TEXT=''
(( ${#RF_UI_NOTICES[@]} )) || return 0
RF_UI_NOTICE_TEXT=$'\n'
for n in "${RF_UI_NOTICES[@]}"; do
RF_UI_NOTICE_TEXT+=" $n"$'\n'
done
RF_UI_NOTICES=()
}
# rf_ui_confirm <question>
rf_ui_confirm() {
local key hint yes
# TRANSLATORS: the letter after "[" is the key for yes. y works too.
hint="$(rf_msg "[y/N]")"
yes="${hint:1:1}"
printf '\n %s %s ' "$1" "$hint"
key="$(rf_read_key)" || return 1
printf '%s\n' "$key"
[[ ${key,,} == y || ${key,,} == "${yes,,}" ]]
}
# _rf_width <text>
# The columns the text takes, into RF_WIDTH. ${#s} counts characters, but
# Chinese, Japanese and Korean ones take two columns each.
RF_WIDTH=0
_rf_width() {
local wide="${1//[^ -〿぀-ヿ㐀-䶿一-鿿가-힯豈-﫿＀-⦆]/}"
RF_WIDTH=$(( ${#1} + ${#wide} ))
}
# _rf_pad <text> <columns>
# The text, padded with spaces to the columns, into RF_PADDED. printf's %-28s
# pads by bytes, so a label containing "ü" would come out one column short.
RF_PADDED=''
_rf_pad() {
local pad
_rf_width "$1"
pad=$(( $2 - RF_WIDTH ))
(( pad < 1 )) && pad=1
printf -v RF_PADDED '%s%*s' "$1" "$pad" ''
}
# _rf_wrap <width> <text>
# Word wrap, into RF_WRAP_LINES.
RF_WRAP_LINES=()
_rf_wrap() {
local width="$1" word line='' used=0 c i
local -a words
RF_WRAP_LINES=()
read -r -a words <<< "$2"
for word in "${words[@]}"; do
_rf_width "$word"
if (( used > 0 && used + 1 + RF_WIDTH > width )); then
RF_WRAP_LINES+=("$line")
line='' used=0
fi
(( used > 0 )) && line+=' ' used=$(( used + 1 ))
if (( used + RF_WIDTH <= width )); then
line+="$word" used=$(( used + RF_WIDTH ))
continue
fi
# Longer than a line: Chinese and Japanese have no spaces, so break
# between any two characters.
for (( i = 0; i < ${#word}; i++ )); do
c="${word:i:1}"
_rf_width "$c"
if (( used + RF_WIDTH > width )); then
RF_WRAP_LINES+=("$line")
line='' used=0
fi
line+="$c" used=$(( used + RF_WIDTH ))
done
done
[[ -n $line ]] && RF_WRAP_LINES+=("$line")
return 0
}
# _rf_apps_label <count>
_rf_apps_label() {
if [[ $1 == 1 ]]; then
rf_msg "1 app"
else
rf_msg "%d apps" "$1"
fi
}
# rf_start_detached <id> <app>
# Starts an app on its own, so it keeps running when the menu closes.
rf_start_detached() {
if rf_have setsid; then
setsid -f "$RF_SELF" run "$1/$2" > /dev/null 2>&1 < /dev/null
else
nohup "$RF_SELF" run "$1/$2" > /dev/null 2>&1 < /dev/null &
fi
}
# ---------------------------------------------------------------------------
# Status
# ---------------------------------------------------------------------------
# rf_ui_status
# The workspaces, for the head of the menu.
rf_ui_status() {
local id width=0 apps
rf_ws_ids
if (( ${#RF_WS_IDS[@]} == 0 )); then
printf ' %s\n' "$(rf_msg "No workspace yet. Add the one of your company with [1].")"
return 0
fi
for id in "${RF_WS_IDS[@]}"; do
rf_ws_load "$id"
_rf_width "$WS_NAME"
(( RF_WIDTH > width )) && width=$RF_WIDTH
done
for id in "${RF_WS_IDS[@]}"; do
rf_ws_load "$id"
rf_ws_apps_load "$id"
_rf_pad "$WS_NAME" $(( width + 3 ))
apps="$(_rf_apps_label "${#RF_APP_SLUGS[@]}")"
printf ' %s%-10s %s%s%s\n' "$RF_PADDED" "$apps" "$RF_C_DIM" "$WS_ACCOUNT" "$RF_C_RESET"
done
}
# ---------------------------------------------------------------------------
# Settings
# ---------------------------------------------------------------------------
# Format: scope|Key|type|default|label-msgid|choices
# scope user (the settings file), entries (the settings file, and the app
# menu entries are written again), timer (the daily refresh, a
# systemd user timer), or group for a heading, with only the label
# after it
# type bool, or choice (steps through the choices)
RF_SETTINGS=(
"group|Apps"
"user|Share|choice|off|Share a folder|off,documents,downloads,home"
"entries|OpenWith|bool|yes|Open files with the apps"
"user|Scale|choice|auto|Size|auto,100,125,150,175,200"
"user|Sound|bool|yes|Sound"
"user|Graphics|choice|gfx|Graphics|gfx,classic"
"group|Signing in"
"user|MfaWait|choice|auto|Wait for the second MFA prompt|auto,always,never"
"timer|AutoRefresh|bool|yes|Refresh the apps every day"
)
# rf_value_label <Key> <value>
# How a setting's value reads in the menu.
rf_value_label() {
local dir
case "$1:$2" in
Share:off) rf_msg "off" ;;
Share:home) rf_msg "home folder" ;;
Share:documents|Share:downloads)
case "$2" in
documents) rf_msg "Documents" ;;
*) rf_msg "Downloads" ;;
esac
if dir="$(CFG_SHARE="$2" rf_share_dir)"; then
printf ' %s(%s)%s' "$RF_C_DIM" "${dir/#"$HOME"/\~}" "$RF_C_RESET"
else
printf ' %s(%s)%s' "$RF_C_YELLOW" "$(rf_msg "not found")" "$RF_C_RESET"
fi
;;
Scale:auto) rf_auto_scale; rf_msg "automatic (%d %%)" "$RF_AUTO_SCALE" ;;
Scale:*) printf '%s %%' "$2" ;;
Graphics:classic) rf_msg "classic" ;;
Graphics:*) rf_msg "GFX" ;;
MfaWait:always) rf_msg "always" ;;
MfaWait:never) rf_msg "never" ;;
MfaWait:*) rf_msg "automatic" ;;
*) printf '%s' "$2" ;;
esac
}
# rf_setting_help <Key> <value>
# What a setting does, shown under the list for the selected one.
rf_setting_help() {
case "$1:$2" in
Share:off) rf_msg "The apps only see the files on the server. Pick a folder to open and save files of this computer in them." ;;
Share:*) rf_msg "The apps can open and save files in this folder. It shows up as a drive in them." ;;
OpenWith:*) rf_msg "Offers the apps under \"Open with\" for their file types, for example Word for .docx. The folder of the file is shared with the app while it is open." ;;
Scale:*) rf_msg "How big the apps are drawn. Automatic follows the scale of your desktop, so they are sharp and the right size." ;;
Sound:*) rf_msg "Plays the sound of the apps here, for calls and videos." ;;
Graphics:classic) rf_msg "Paints every window right. But some servers end the session right after signing in, then go back to GFX." ;;
Graphics:*) rf_msg "Works with every server. FreeRDP may not repaint a window after it was minimized or resized." ;;
MfaWait:always) rf_msg "Always waits before the second sign-in. For when the second prompt does not come with automatic." ;;
MfaWait:never) rf_msg "Never waits. Right when your company has no MFA." ;;
MfaWait:*) rf_msg "Many servers with MFA need a second sign-in, and its prompt only comes after a short wait. Automatic waits when the first sign-in took a while." ;;
AutoRefresh:*)
if rf_timer_available; then
rf_msg "Loads the list of apps again once a day, so new apps show up by themselves."
else
rf_msg "Needs a systemd user session."
fi
;;
esac
}
# _rf_setting_value <scope> <Key> <default>
# The current value, in RF_SETTING_VALUE.
RF_SETTING_VALUE=''
_rf_setting_value() {
case "$1" in
user|entries) _rf_kv_lookup "$RF_CONFIG" "$2" "$3"; RF_SETTING_VALUE="$RF_KV_VALUE" ;;
timer) if rf_timer_enabled; then RF_SETTING_VALUE=yes; else RF_SETTING_VALUE=no; fi ;;
esac
}
# _rf_step <current> <step> <choice>...
# The choice <step> (1 or -1) away from the current one, round at the ends.
_rf_step() {
local current="$1" step="$2" i
shift 2
local -a all=("$@")
for i in "${!all[@]}"; do
if [[ ${all[i]} == "$current" ]]; then
printf '%s\n' "${all[(i + step + ${#all[@]}) % ${#all[@]}]}"
return
fi
done
printf '%s\n' "${all[0]}"
}
# _rf_setting_change <scope> <Key> <type> <current> <choices> <step>
_rf_setting_change() {
local scope="$1" key="$2" type="$3" current="$4" options="$5" step="$6" next
local -a choices
case "$type" in
bool) if rf_is_true "$current"; then next=no; else next=yes; fi ;;
choice)
IFS=',' read -r -a choices <<< "$options"
next="$(_rf_step "$current" "$step" "${choices[@]}")"
;;
esac
case "$scope" in
user)
rf_config_set "$key" "$next" || rf_bad "$(rf_msg "Could not save the setting.")"
;;
entries)
rf_config_set "$key" "$next" || rf_bad "$(rf_msg "Could not save the setting.")"
rf_ws_entries_all
;;
timer)
rf_config_set "$key" "$next"
rf_timer_set "$next" || rf_bad "$(rf_msg "Needs a systemd user session.")"
;;
esac
}
# rf_ui_settings
# A cursor list in groups, with what the selected setting does under it. The
# frame is assembled in memory and written once, and everything constant is
# resolved before the loop.
rf_ui_settings() {
local -a scopes=() keys=() types=() defaults=() labels=() widths=() choices=() values=() rows=()
local spec scope key type default label choice locale i j frame row pad dirty=1 cursor=0 shown
local width=0 wrap cols
locale="$(rf_ui_locale)"
for spec in "${RF_SETTINGS[@]}"; do
IFS='|' read -r scope key type default label choice <<< "$spec"
# A heading has its label where the key would be.
[[ $scope == group ]] && label="$key" key=''
scopes+=("$scope"); keys+=("$key"); types+=("$type"); defaults+=("$default")
choices+=("$choice")
rf_msg_into "$locale" "$label"
labels+=("$RF_MSG_RESULT")
_rf_width "$RF_MSG_RESULT"
widths+=("$RF_WIDTH")
if [[ $scope != group ]]; then
rows+=($(( ${#keys[@]} - 1 )))
(( RF_WIDTH > width )) && width=$RF_WIDTH
fi
done
local count=${#rows[@]}
cols="$(tput cols 2>/dev/null)" || cols=80
[[ $cols =~ ^[0-9]+$ ]] || cols=80
wrap=$(( cols - 4 ))
(( wrap > 72 )) && wrap=72
local rule
printf -v rule '%*s' "$wrap" ''
rule="${rule// /─}"
local title hint l_on l_off
rf_msg_into "$locale" "Settings"; title="$RF_MSG_RESULT"
rf_msg_into "$locale" "↑↓ select ←→ or Space: change q: back"; hint="$RF_MSG_RESULT"
rf_msg_into "$locale" "ON"; l_on="$RF_MSG_RESULT"
rf_msg_into "$locale" "OFF"; l_off="$RF_MSG_RESULT"
local clearseq
clearseq="$(clear 2>/dev/null)" || clearseq=$'\033[H\033[2J'
while true; do
if (( dirty )); then
RF_KV_CACHE=()
for i in "${rows[@]}"; do
_rf_setting_value "${scopes[i]}" "${keys[i]}" "${defaults[i]}"
values[i]="$RF_SETTING_VALUE"
done
dirty=0
fi
frame="$clearseq"$'\n'"${RF_C_BOLD}${RF_C_BLUE} ${title}${RF_C_RESET}"$'\n'
local selected=${rows[cursor]} marker before after
for i in "${!keys[@]}"; do
if [[ ${scopes[i]} == group ]]; then
frame+=$'\n'" ${RF_C_BOLD}${labels[i]}${RF_C_RESET}"$'\n'
continue
fi
if [[ ${types[i]} == bool ]]; then
if rf_is_true "${values[i]}"; then
shown="${RF_C_GREEN}${l_on}${RF_C_RESET}"
else
shown="${RF_C_DIM}${l_off}${RF_C_RESET}"
fi
else
shown="$(rf_value_label "${keys[i]}" "${values[i]}")"
fi
# Arrows on the selected choice: left and right step through it.
before=' ' after=''
if (( i == selected )) && [[ ${types[i]} != bool ]]; then
before="${RF_C_BLUE}◂${RF_C_RESET} " after=" ${RF_C_BLUE}▸${RF_C_RESET}"
fi
pad=$(( width + 2 - widths[i] ))
if (( i == selected )); then marker="${RF_C_BLUE}▸${RF_C_RESET} "; else marker=' '; fi
printf -v row ' %s%s%*s%s%s%s' "$marker" "${labels[i]}" "$pad" '' "$before" "$shown$RF_C_RESET" "$after"
frame+="$row"$'\n'
done
# What the selected setting does, in a box of fixed height so the
# screen does not jump while moving through the list.
_rf_wrap "$wrap" "$(rf_setting_help "${keys[selected]}" "${values[selected]}")"
frame+=$'\n'" ${RF_C_DIM}${rule}${RF_C_RESET}"$'\n'
for j in 0 1 2; do
frame+=" ${RF_WRAP_LINES[j]:-}"$'\n'
done
frame+=$'\n'" ${RF_C_DIM}${hint}${RF_C_RESET}"$'\n'
_rf_ui_take_notices
frame+="$RF_UI_NOTICE_TEXT"
printf '%s' "$frame"
key="$(rf_read_key)" || return 0
case "$key" in
up|k) cursor=$(( (cursor - 1 + count) % count )) ;;
down|j) cursor=$(( (cursor + 1) % count )) ;;
space|right|l|left|h)
local step=1
[[ $key == left || $key == h ]] && step=-1
_rf_setting_change "${scopes[selected]}" "${keys[selected]}" "${types[selected]}" "${values[selected]}" "${choices[selected]}" "$step"
dirty=1
;;
q|Q|escape) return 0 ;;
*) ;;
esac
done
}
# ---------------------------------------------------------------------------
# Workspaces
# ---------------------------------------------------------------------------
rf_ui_workspaces() {
local key frame row i cursor=0 count locale width
local -a ids=()
locale="$(rf_ui_locale)"
local title hint empty
rf_msg_into "$locale" "Workspaces"; title="$RF_MSG_RESULT"
# TRANSLATORS: keep the letters, they are the keys.
rf_msg_into "$locale" "↑↓ select r: refresh s: sign in again d: remove a: add q: back"; hint="$RF_MSG_RESULT"
rf_msg_into "$locale" "No workspace yet. Press a to add one."; empty="$RF_MSG_RESULT"
local clearseq
clearseq="$(clear 2>/dev/null)" || clearseq=$'\033[H\033[2J'
while true; do
rf_ws_ids
ids=("${RF_WS_IDS[@]}")
count=${#ids[@]}
(( cursor >= count )) && cursor=$(( count > 0 ? count - 1 : 0 ))
width=0
for i in "${!ids[@]}"; do
rf_ws_load "${ids[i]}"
_rf_width "$WS_NAME"
(( RF_WIDTH > width )) && width=$RF_WIDTH
done
frame="$clearseq"$'\n'"${RF_C_BOLD}${RF_C_BLUE} ${title}${RF_C_RESET}"$'\n\n'
(( count == 0 )) && frame+=" ${RF_C_DIM}${empty}${RF_C_RESET}"$'\n'
local marker apps when
for i in "${!ids[@]}"; do
rf_ws_load "${ids[i]}"
rf_ws_apps_load "${ids[i]}"
apps="$(_rf_apps_label "${#RF_APP_SLUGS[@]}")"
when="$(rf_msg "refreshed %s" "$(rf_time_ago "$WS_REFRESHED")")"
_rf_pad "$WS_NAME" $(( width + 3 ))
if (( i == cursor )); then marker="${RF_C_BLUE}▸${RF_C_RESET} "; else marker=' '; fi
printf -v row ' %s%s%-10s %s %s%s%s' "$marker" "$RF_PADDED" "$apps" "$WS_ACCOUNT" "$RF_C_DIM" "$when" "$RF_C_RESET"
frame+="$row"$'\n'
done
frame+=$'\n'" ${RF_C_DIM}${hint}${RF_C_RESET}"$'\n'
_rf_ui_take_notices
frame+="$RF_UI_NOTICE_TEXT"
printf '%s' "$frame"
key="$(rf_read_key)" || return 0
case "$key" in
up|k) (( count )) && cursor=$(( (cursor - 1 + count) % count )) ;;
down|j) (( count )) && cursor=$(( (cursor + 1) % count )) ;;
r|R)
(( count )) || continue
printf '\n'
rf_ui_cooked rf_do_refresh "${ids[cursor]}"
;;
s|S)
(( count )) || continue
printf '\n'
rf_ui_cooked rf_do_signin_again "${ids[cursor]}"
;;
d|D)
(( count )) || continue
rf_ws_load "${ids[cursor]}"
if rf_ui_confirm "$(rf_msg "Remove \"%s\" and its apps?" "$WS_NAME")"; then
rf_do_remove "${ids[cursor]}" > /dev/null
fi
;;
a|A)
printf '\n'
rf_ui_cooked rf_do_add
;;
q|Q|escape) return 0 ;;
*) ;;
esac
done
}
# ---------------------------------------------------------------------------
# Apps
# ---------------------------------------------------------------------------
rf_ui_apps() {
local key frame row i id cursor=0 count locale
local -a ws=() slugs=() titles=()
locale="$(rf_ui_locale)"
local title hint empty
rf_msg_into "$locale" "Start an app"; title="$RF_MSG_RESULT"
rf_msg_into "$locale" "↑↓ select Enter: start q: back"; hint="$RF_MSG_RESULT"
rf_msg_into "$locale" "No apps yet. Add a workspace first."; empty="$RF_MSG_RESULT"
rf_ws_ids
for id in "${RF_WS_IDS[@]}"; do
rf_ws_apps_load "$id"
for i in "${!RF_APP_SLUGS[@]}"; do
ws+=("$id") slugs+=("${RF_APP_SLUGS[i]}") titles+=("${RF_APP_TITLES[i]}")
done
done
count=${#slugs[@]}
local clearseq
clearseq="$(clear 2>/dev/null)" || clearseq=$'\033[H\033[2J'
while true; do
frame="$clearseq"$'\n'"${RF_C_BOLD}${RF_C_BLUE} ${title}${RF_C_RESET}"$'\n'
(( count == 0 )) && frame+=$'\n'" ${RF_C_DIM}${empty}${RF_C_RESET}"$'\n'
local marker last=''
for i in "${!slugs[@]}"; do
# A heading for each workspace.
if [[ ${ws[i]} != "$last" ]]; then
rf_ws_load "${ws[i]}"
frame+=$'\n'" ${RF_C_BOLD}${WS_NAME}${RF_C_RESET}"$'\n'
last="${ws[i]}"
fi
if (( i == cursor )); then marker="${RF_C_BLUE}▸${RF_C_RESET} "; else marker=' '; fi
printf -v row ' %s%s' "$marker" "${titles[i]}"
frame+="$row"$'\n'
done
frame+=$'\n'" ${RF_C_DIM}${hint}${RF_C_RESET}"$'\n'
_rf_ui_take_notices
frame+="$RF_UI_NOTICE_TEXT"
printf '%s' "$frame"
key="$(rf_read_key)" || return 0
case "$key" in
up|k) (( count )) && cursor=$(( (cursor - 1 + count) % count )) ;;
down|j) (( count )) && cursor=$(( (cursor + 1) % count )) ;;
enter|space)
(( count )) || continue
rf_start_detached "${ws[cursor]}" "${slugs[cursor]}"
rf_ok "$(rf_msg "%s is starting. It opens in a window of its own." "${titles[cursor]}")" > /dev/null
;;
q|Q|escape) return 0 ;;
*) ;;
esac
done
}
# ---------------------------------------------------------------------------
# The menu
# ---------------------------------------------------------------------------
rf_ui_menu() {
local choice
if [[ ! -t 0 ]]; then
rf_do_help
return 1
fi
rf_ui_term_raw
trap 'rf_ui_term_restore' EXIT INT TERM
while true; do
clear 2>/dev/null || true
rf_head " $RF_PRETTY"
rf_ui_status
printf '\n'
printf ' [1] %s\n' "$(rf_msg "Add a workspace")"
printf ' [2] %s\n' "$(rf_msg "Start an app")"
printf ' [3] %s\n' "$(rf_msg "Workspaces")"
printf ' [4] %s\n' "$(rf_msg "Settings")"
printf ' [q] %s\n' "$(rf_msg "Quit")"
_rf_ui_take_notices
printf '%s' "$RF_UI_NOTICE_TEXT"
printf '\n > '
choice="$(rf_read_key)" || {
printf '\n'; rf_ui_term_restore; trap - EXIT INT TERM; return 0
}
case "$choice" in
enter|space|up|down|left|right|escape) choice='' ;;
esac
printf '%s\n' "$choice"
case "$choice" in
1) printf '\n'; rf_ui_cooked rf_do_add ;;
2) rf_ui_apps ;;
3) rf_ui_workspaces ;;
4) rf_ui_settings ;;
q|Q) rf_ui_term_restore; trap - EXIT INT TERM; return 0 ;;
# Anything else (Enter, arrow keys, stray characters) just
# redraws. Escape is deliberately not a quit key, so a mistyped
# arrow key cannot close the menu.
*) ;;
esac
done
}
+389
View File
@@ -0,0 +1,389 @@
# shellcheck shell=bash
#
# Workspaces: the feeds rdfeed is subscribed to, and the app menu entries they
# turn into.
#
# A workspace has an id made from its server name. Its settings are in
# ~/.config/rdfeed/workspaces/<id>, its apps in ~/.local/share/rdfeed/<id>
# (apps.tsv, rdp/<app>.rdp, icons/<app>.png), its password in the keyring.
# Each app gets ~/.local/share/applications/rdfeed-<id>--<app>.desktop, and
# the workspace a folder of its own in the app menu. With "Open files with
# the apps" on, the entries also name the file types, so file managers offer
# the apps under "Open with".
# ---------------------------------------------------------------------------
# The keyring
# ---------------------------------------------------------------------------
rf_secret_get() {
rf_have secret-tool || return 1
secret-tool lookup service rdfeed workspace "$1" user "$2" 2>/dev/null
}
# rf_secret_set <id> <account> <label> <password>
# secret-tool keeps all of stdin, a trailing newline too, so printf and not
# a here-string. printf is a builtin: the password shows in no process list.
rf_secret_set() {
rf_have secret-tool || return 1
printf '%s' "$4" | secret-tool store --label="rdfeed: $3" service rdfeed workspace "$1" user "$2" \
> /dev/null 2>&1
}
rf_secret_clear() {
rf_have secret-tool || return 0
secret-tool clear service rdfeed workspace "$1" > /dev/null 2>&1 || true
}
# ---------------------------------------------------------------------------
# Finding the feed
# ---------------------------------------------------------------------------
# rf_discover <domain>
# Windows finds the feed for a work e-mail address in a TXT record of the
# domain, _msradc.<domain>. Whichever DNS tool is around asks for it.
rf_discover() {
local name="_msradc.$1" txt=''
rf_have resolvectl && txt="$(resolvectl query --legend=no -t TXT "$name" 2>/dev/null)"
[[ -z $txt ]] && rf_have dig && txt="$(dig +short TXT "$name" 2>/dev/null)"
[[ -z $txt ]] && rf_have host && txt="$(host -t TXT "$name" 2>/dev/null)"
[[ -z $txt ]] && rf_have drill && txt="$(drill TXT "$name" 2>/dev/null)"
[[ $txt =~ (https?://[^\"[:space:]]+) ]] || return 1
printf '%s\n' "${BASH_REMATCH[1]}"
}
# rf_feed_url <what was typed>
# A server name, an address or a work e-mail address, as the address of the
# feed, in RF_URL. 1: nothing typed, 2: no feed found for the e-mail address.
RF_URL=''
rf_feed_url() {
local in="$1" scheme rest host path
in="${in#"${in%%[![:space:]]*}"}"
in="${in%"${in##*[![:space:]]}"}"
RF_URL=''
[[ -n $in ]] || return 1
if [[ $in != */* && $in == *@* ]]; then
RF_URL="$(rf_discover "${in#*@}")" || return 2
return 0
fi
[[ $in == *://* ]] || in="https://$in"
scheme="${in%%://*}"
rest="${in#*://}"
host="${rest%%/*}"
path="${rest#"$host"}"
# The feed sits at the same place on every server. Whatever part of it
# was typed, the RD Web page included, leads there.
case "${path,,}" in
''|/|/rdweb|/rdweb/|/rdweb/feed|/rdweb/feed/|/rdweb/pages*) path=/RDWeb/Feed/webfeed.aspx ;;
*/webfeed) path+=.aspx ;;
esac
RF_URL="${scheme,,}://$host$path"
}
# rf_ws_id_for <url>
# The workspace that has this address, or a new id made from the server name.
rf_ws_id_for() {
local url="$1" id base n=2 host
rf_ws_ids
for id in "${RF_WS_IDS[@]}"; do
_rf_kv_lookup "$(rf_ws_file "$id")" Url
[[ ${RF_KV_VALUE,,} == "${url,,}" ]] && { printf '%s\n' "$id"; return; }
done
host="${url#*://}"
host="${host%%/*}"
host="${host%%:*}"
base="${host,,}"
base="${base//[^a-z0-9]/-}"
while [[ $base == *--* ]]; do base="${base//--/-}"; done
base="${base#-}"
base="${base%-}"
[[ -n $base ]] || base=workspace
id="$base"
while [[ -e $(rf_ws_file "$id") ]]; do
id="$base-$n"
n=$(( n + 1 ))
done
printf '%s\n' "$id"
}
# rf_split_user <typed>
# DOMAIN\name, name@domain or just a name, into RF_USER and RF_DOMAIN. The
# domain is "-" when the server's own should be taken, and empty for
# name@domain, which carries its domain.
rf_split_user() {
local in="$1"
if [[ $in == *\\* ]]; then
RF_DOMAIN="${in%%\\*}"
RF_USER="${in#*\\}"
elif [[ $in == *@* ]]; then
RF_DOMAIN=''
RF_USER="$in"
else
RF_DOMAIN='-'
RF_USER="$in"
fi
}
# ---------------------------------------------------------------------------
# Downloading
# ---------------------------------------------------------------------------
# rf_fetch <url> <user> <domain or -> <dir> <password> [quiet]
# Runs rdfeed-fetch into <dir>, printing each app as it arrives unless quiet.
# Leaves RF_FETCH_NAME, RF_FETCH_DOMAIN, RF_FETCH_COUNT and, when it failed,
# RF_FETCH_ERROR (the reason, already translated).
rf_fetch() {
local url="$1" user="$2" domain="$3" out="$4" password="$5" quiet="${6:-}"
local kind a b c rc=1 detail
local -a args=(--url "$url" --user "$user" --out "$out")
[[ $domain != - ]] && args+=(--domain "$domain")
RF_FETCH_NAME='' RF_FETCH_DOMAIN='' RF_FETCH_COUNT=0 RF_FETCH_ERROR=''
while IFS=$'\t' read -r kind a b c; do
case "$kind" in
workspace) RF_FETCH_NAME="$a" ;;
domain) RF_FETCH_DOMAIN="$a" ;;
app)
RF_FETCH_COUNT=$(( RF_FETCH_COUNT + 1 ))
[[ -z $quiet ]] && printf ' %s✔%s %s\n' "$RF_C_GREEN" "$RF_C_RESET" "$c"
;;
skip)
[[ -z $quiet ]] && printf ' %s✘%s %s %s(%s)%s\n' "$RF_C_RED" "$RF_C_RESET" "$a" "$RF_C_DIM" "$b" "$RF_C_RESET"
;;
exit) rc="$a" ;;
esac
done < <("$RF_FETCH" "${args[@]}" <<< "$password" 2> "$out/.error"; printf 'exit\t%s\n' "$?")
(( rc == 0 )) && { rm -f "$out/.error"; return 0; }
detail="$(tail -n1 "$out/.error" 2>/dev/null)"
detail="${detail#rdfeed-fetch: }"
case "$rc" in
10) RF_FETCH_ERROR="$(rf_msg "The server did not accept the user name or password.")" ;;
11) RF_FETCH_ERROR="$(rf_msg "Could not reach the server: %s" "$detail")" ;;
12) RF_FETCH_ERROR="$(rf_msg "There is no RD Web feed at %s." "$url")" ;;
*) RF_FETCH_ERROR="$(rf_msg "Could not load the apps: %s" "${detail:-$rc}")" ;;
esac
return "$rc"
}
# rf_stage_dir
# A fresh directory next to the workspaces, so moving it in place is a rename.
rf_stage_dir() {
mkdir -p "$RF_DATADIR" && mktemp -d "$RF_DATADIR/.new.XXXXXX"
}
# rf_ws_install <id> <stage dir>
rf_ws_install() {
local dir="$RF_DATADIR/$1"
rm -f "$2/.error"
rm -rf "$dir.old"
[[ -d $dir ]] && mv "$dir" "$dir.old"
mv "$2" "$dir" || { [[ -d $dir.old ]] && mv "$dir.old" "$dir"; return 1; }
rm -rf "$dir.old"
rf_ws_entries "$1"
}
# rf_ws_apps_load <id>
# The apps of a workspace in RF_APP_SLUGS, RF_APP_TYPES, RF_APP_TITLES and
# RF_APP_EXTS (the file types it opens, as "docx doc rtf").
RF_APP_SLUGS=() RF_APP_TYPES=() RF_APP_TITLES=() RF_APP_EXTS=()
rf_ws_apps_load() {
local slug type title exts file="$RF_DATADIR/$1/apps.tsv"
RF_APP_SLUGS=() RF_APP_TYPES=() RF_APP_TITLES=() RF_APP_EXTS=()
[[ -r $file ]] || return 0
while IFS=$'\t' read -r slug type title exts; do
[[ -n $slug ]] || continue
RF_APP_SLUGS+=("$slug") RF_APP_TYPES+=("$type") RF_APP_TITLES+=("$title") RF_APP_EXTS+=("$exts")
done < "$file"
}
# ---------------------------------------------------------------------------
# The app menu
# ---------------------------------------------------------------------------
_rf_desktop_value() {
local v="${1//\\/\\\\}"
v="${v//$'\n'/ }"
printf '%s' "${v//$'\r'/}"
}
# The command, as the Exec line of an entry wants it.
_rf_exec_path() {
if [[ $RF_SELF =~ ^[A-Za-z0-9_./+-]+$ ]]; then
printf '%s' "$RF_SELF"
else
local p="${RF_SELF//\\/\\\\\\\\}"
p="${p//\"/\\\\\"}"
p="${p//\$/\\\\\$}"
p="${p//\`/\\\\\`}"
printf '"%s"' "$p"
fi
}
# _rf_mime_types <ext>...
# The MIME types of the file extensions, as a MimeType= value, from the
# shared-mime-info database every desktop uses.
_rf_mime_types() {
local f
local -a globs=()
for f in "$RF_XDG_DATA/mime/globs2" /usr/local/share/mime/globs2 /usr/share/mime/globs2; do
[[ -r $f ]] && globs+=("$f")
done
(( ${#globs[@]} )) || return 0
awk -F: -v want=" $* " '
BEGIN { want = tolower(want) }
/^#/ { next }
{
glob = tolower($3)
if (glob !~ /^\*\.[a-z0-9_+-]+$/) next
if (index(want, " " substr(glob, 3) " ") && !seen[$2]++) out = out $2 ";"
}
END { printf "%s", out }
' "${globs[@]}"
}
# rf_menu_refresh
# KDE reads the app menu from a cache, which needs a nudge. GNOME and the rest
# watch the folders themselves.
rf_menu_refresh() {
if rf_have kbuildsycoca6; then
kbuildsycoca6 > /dev/null 2>&1 || true
elif rf_have kbuildsycoca5; then
kbuildsycoca5 > /dev/null 2>&1 || true
fi
rf_have update-desktop-database && update-desktop-database "$RF_APPDIR" > /dev/null 2>&1
return 0
}
# rf_ws_entries <id>
# An entry for each app, a folder for the workspace, and nothing left of apps
# the feed no longer has.
rf_ws_entries() {
local id="$1" i slug title icon comment file keywords mime openwith=no args
local -A keep=()
rf_ws_load "$id"
rf_ws_apps_load "$id"
mkdir -p "$RF_APPDIR" "$RF_MENUDIR" "$RF_DIRDIR" || return 1
_rf_kv_lookup "$RF_CONFIG" OpenWith yes
rf_is_true "$RF_KV_VALUE" && openwith=yes
keywords="$(_rf_desktop_value "$WS_NAME")"
keywords="${keywords//;/\\;}"
for i in "${!RF_APP_SLUGS[@]}"; do
slug="${RF_APP_SLUGS[i]}"
title="${RF_APP_TITLES[i]}"
icon="$RF_DATADIR/$id/icons/$slug.png"
[[ -f $icon ]] || icon=rdfeed
if [[ ${RF_APP_TYPES[i]} == Desktop ]]; then
comment="$(rf_msg "Remote desktop of %s" "$WS_NAME")"
else
comment="$(rf_msg "%s from %s" "$title" "$WS_NAME")"
fi
mime='' args=''
if [[ $openwith == yes && -n ${RF_APP_EXTS[i]} ]]; then
# shellcheck disable=SC2086 # one extension per argument
mime="$(_rf_mime_types ${RF_APP_EXTS[i]})"
[[ -n $mime ]] && args=' %f'
fi
file="$RF_APPDIR/rdfeed-$id--$slug.desktop"
keep[$file]=1
cat > "$file" <<- EOF
[Desktop Entry]
Type=Application
Name=$(_rf_desktop_value "$title")
GenericName=$(_rf_desktop_value "$WS_NAME")
Comment=$(_rf_desktop_value "$comment")
Exec=$(_rf_exec_path) run $id/$slug$args
Icon=$icon
Terminal=false
StartupNotify=false
Categories=X-rdfeed-$id;
Keywords=RemoteApp;RDP;$keywords;
EOF
[[ -n $mime ]] && printf 'MimeType=%s\n' "$mime" >> "$file"
done
for file in "$RF_APPDIR"/rdfeed-"$id"--*.desktop; do
[[ -e $file && -z ${keep[$file]:-} ]] && rm -f "$file"
done
cat > "$RF_MENUDIR/rdfeed-$id.menu" <<- EOF
<!DOCTYPE Menu PUBLIC "-//freedesktop//DTD Menu 1.0//EN"
"http://www.freedesktop.org/standards/menu-spec/menu-1.0.dtd">
<!-- Written by rdfeed: the folder of the workspace $id. -->
<Menu>
<Name>Applications</Name>
<Menu>
<Name>rdfeed-$id</Name>
<Directory>rdfeed-$id.directory</Directory>
<Include>
<Category>X-rdfeed-$id</Category>
</Include>
</Menu>
</Menu>
EOF
cat > "$RF_DIRDIR/rdfeed-$id.directory" <<- EOF
[Desktop Entry]
Type=Directory
Name=$(_rf_desktop_value "$WS_NAME")
Icon=rdfeed
EOF
rf_menu_refresh
}
# rf_ws_entries_all
# The entries of every workspace again, after a setting that shows in them.
rf_ws_entries_all() {
local id
rf_ws_ids
for id in "${RF_WS_IDS[@]}"; do
rf_ws_entries "$id"
done
}
# rf_ws_purge <id>
# Everything of a workspace: entries, folder, apps, settings, password, logs.
rf_ws_purge() {
local id="$1" file
for file in "$RF_APPDIR"/rdfeed-"$id"--*.desktop; do
[[ -e $file ]] && rm -f "$file"
done
rm -f "$RF_MENUDIR/rdfeed-$id.menu" "$RF_DIRDIR/rdfeed-$id.directory"
rm -rf "${RF_DATADIR:?}/$id"
rm -f "$(rf_ws_file "$id")"
for file in "$RF_CACHEDIR/$id"--*.log; do
[[ -e $file ]] && rm -f "$file"
done
rf_secret_clear "$id"
rf_menu_refresh
}
# ---------------------------------------------------------------------------
# Refreshing every day
# ---------------------------------------------------------------------------
rf_timer_available() {
rf_have systemctl || return 1
systemctl --user cat "$RF_UNIT_TIMER" > /dev/null 2>&1
}
rf_timer_enabled() {
rf_have systemctl || return 1
[[ $(systemctl --user is-enabled "$RF_UNIT_TIMER" 2>/dev/null) == enabled ]]
}
# rf_timer_set yes|no
rf_timer_set() {
rf_timer_available || return 1
if [[ $1 == yes ]]; then
systemctl --user enable --now "$RF_UNIT_TIMER" > /dev/null 2>&1
else
systemctl --user disable --now "$RF_UNIT_TIMER" > /dev/null 2>&1
fi
}
Executable
+321
View File
@@ -0,0 +1,321 @@
#!/usr/bin/env bash
#
# rdfeed: the RemoteApps of your company in the Linux app menu
#
# Companies publish their Windows apps (Word, Excel, their own tools) as an RD
# Web Access feed. Windows subscribes to it under "RemoteApp and Desktop
# Connections" and puts the apps in the start menu. rdfeed does the same on
# Linux: each app gets an entry in the app menu and opens as a window of its
# own, through FreeRDP.
#
# This is the front end: the menu and the commands. Signing in to the feed and
# downloading the apps is rdfeed-fetch's job, and rdfeed-hook.so goes into
# FreeRDP to work around two of its problems (see launch.sh).
#
# Copyright (C) 2026 Felitendo
# SPDX-License-Identifier: GPL-3.0-or-later
set -uo pipefail
RF_SELF="$(readlink -f "${BASH_SOURCE[0]}")"
RF_LIBDIR="${RF_LIBDIR:-@LIBDIR@}"
for _mod in common config workspace launch menu; do
# shellcheck source=/dev/null
if ! source "$RF_LIBDIR/$_mod.sh"; then
printf 'rdfeed: cannot load %s/%s.sh\n' "$RF_LIBDIR" "$_mod" >&2
exit 14
fi
done
unset _mod
# ---------------------------------------------------------------------------
# Commands
# ---------------------------------------------------------------------------
# rf_prompt <question> [default]
# A line typed in, in RF_ANSWER. Without a terminal it is read from stdin, so
# scripts can pipe the answers in.
RF_ANSWER=''
rf_prompt() {
RF_ANSWER=''
if [[ -t 0 ]]; then
IFS= read -rep " $1 " -i "${2:-}" RF_ANSWER || return 1
else
IFS= read -r RF_ANSWER || return 1
fi
return 0
}
# rf_prompt_password <question>
rf_prompt_password() {
RF_ANSWER=''
if [[ -t 0 ]]; then
IFS= read -rsp " $1 " RF_ANSWER || return 1
printf '\n'
else
IFS= read -r RF_ANSWER || return 1
fi
[[ -n $RF_ANSWER ]]
}
# rf_signin <id or ""> <url> <user as typed> <password>
# Signs in, downloads the apps and puts them in the app menu. With an empty
# id a new workspace is made, or the one that already has this address.
rf_signin() {
local id="$1" url="$2" typed="$3" password="$4" stage old_account first=0 domain
[[ -n $id ]] || id="$(rf_ws_id_for "$url")"
rf_ws_ids
(( ${#RF_WS_IDS[@]} == 0 )) && first=1
old_account=''
if [[ -e $(rf_ws_file "$id") ]]; then
rf_ws_load "$id"
old_account="$WS_ACCOUNT"
fi
rf_split_user "$typed"
stage="$(rf_stage_dir)" || { rf_bad "$(rf_msg "Could not write to %s." "$RF_DATADIR")"; return 1; }
local host="${url#*://}"
rf_say " $(rf_msg "Signing in to %s…" "${host%%/*}")"
if ! rf_fetch "$url" "$RF_USER" "$RF_DOMAIN" "$stage" "$password"; then
rm -rf "$stage"
rf_bad "$RF_FETCH_ERROR"
return 1
fi
domain="$RF_DOMAIN"
[[ $domain == - ]] && domain="$RF_FETCH_DOMAIN"
rf_ws_set "$id" Url "$url"
rf_ws_set "$id" Name "$RF_FETCH_NAME"
rf_ws_set "$id" User "$RF_USER"
rf_ws_set "$id" Domain "$domain"
rf_ws_set "$id" Refreshed "$(date +%s)"
rf_ws_load "$id"
[[ -n $old_account && $old_account != "$WS_ACCOUNT" ]] && rf_secret_clear "$id"
if ! rf_secret_set "$id" "$WS_ACCOUNT" "$WS_NAME" "$password"; then
rf_note "$(rf_msg "The password could not be saved in the keyring, so each app asks for it.")"
fi
rf_ws_install "$id" "$stage" || { rf_bad "$(rf_msg "Could not write to %s." "$RF_DATADIR")"; return 1; }
# New apps should show up by themselves, as on Windows.
if (( first )) && [[ $(rf_config_get AutoRefresh yes) == yes ]]; then
rf_timer_set yes || true
fi
if (( RF_FETCH_COUNT == 1 )); then
rf_ok "$(rf_msg "1 app from %s is now in your app menu." "$WS_NAME")"
else
rf_ok "$(rf_msg "%d apps from %s are now in your app menu." "$RF_FETCH_COUNT" "$WS_NAME")"
fi
}
rf_do_add() {
local input='' user='' url rc
while (( $# )); do
case "$1" in
--user) user="${2:-}"; shift ;;
*) input="$1" ;;
esac
shift
done
if [[ -z $input ]]; then
rf_say " $(rf_msg "Your IT department has the address. A work e-mail address often works too.")"
rf_prompt "$(rf_msg "Feed address, server or e-mail:")" || return 1
input="$RF_ANSWER"
fi
rf_feed_url "$input"
rc=$?
case "$rc" in
1) return 1 ;;
2) rf_bad "$(rf_msg "No feed is registered for %s. Ask your IT department for the address." "$input")"; return 1 ;;
esac
url="$RF_URL"
if [[ -z $user ]]; then
rf_prompt "$(rf_msg "User name (DOMAIN\\name or name@domain):")" || return 1
user="$RF_ANSWER"
fi
[[ -n $user ]] || return 1
rf_prompt_password "$(rf_msg "Password:")" || return 1
rf_signin '' "$url" "$user" "$RF_ANSWER"
}
# rf_do_signin_again <id>
# Another password or another user for a workspace.
rf_do_signin_again() {
local id="$1"
rf_ws_load "$id"
rf_prompt "$(rf_msg "User name (DOMAIN\\name or name@domain):")" "$WS_ACCOUNT" || return 1
[[ -n $RF_ANSWER ]] || return 1
local user="$RF_ANSWER"
rf_prompt_password "$(rf_msg "Password:")" || return 1
rf_signin "$id" "$WS_URL" "$user" "$RF_ANSWER"
}
# rf_do_refresh [id] [--quiet]
# The quiet one is the daily timer's: no questions, and a notification only
# when somebody has to do something.
rf_do_refresh() {
local quiet='' only='' id password stage rc failed=0
while (( $# )); do
case "$1" in
--quiet) quiet=1 ;;
*) only="$1" ;;
esac
shift
done
rf_ws_ids
if (( ${#RF_WS_IDS[@]} == 0 )); then
[[ -z $quiet ]] && rf_note "$(rf_msg "No workspace yet. Add one with \`%s add\`." "$RF_NAME")"
return 0
fi
for id in "${RF_WS_IDS[@]}"; do
[[ -n $only && $id != "$only" ]] && continue
rf_ws_load "$id"
password="$(rf_secret_get "$id" "$WS_ACCOUNT")" || password=''
if [[ -z $password ]]; then
if [[ -n $quiet || ! -t 0 ]]; then
[[ -n $quiet ]] && rf_notify "$WS_NAME" "$(rf_msg "No password saved for %s. Sign in again in rdfeed." "$WS_NAME")" dialog-warning
failed=1
continue
fi
rf_prompt_password "$(rf_msg "Password for %s:" "$WS_ACCOUNT")" || { failed=1; continue; }
password="$RF_ANSWER"
fi
[[ -z $quiet ]] && rf_say " $(rf_msg "Refreshing %s…" "$WS_NAME")"
stage="$(rf_stage_dir)" || return 1
rf_fetch "$WS_URL" "$WS_USER" "${WS_DOMAIN:-}" "$stage" "$password" "$quiet"
rc=$?
if (( rc != 0 )); then
rm -rf "$stage"
failed=1
if [[ -n $quiet ]]; then
# A server that is not reachable now will be tomorrow. A
# password that no longer works needs the user.
(( rc == 10 )) && rf_notify "$WS_NAME" "$(rf_msg "The password no longer works. Sign in again in rdfeed.")" dialog-warning
else
rf_bad "$RF_FETCH_ERROR"
fi
continue
fi
[[ -n $RF_FETCH_NAME ]] && rf_ws_set "$id" Name "$RF_FETCH_NAME"
rf_ws_set "$id" Refreshed "$(date +%s)"
rf_ws_install "$id" "$stage" || { failed=1; continue; }
[[ -z $quiet ]] && rf_ok "$(rf_msg "%s: %d apps" "$WS_NAME" "$RF_FETCH_COUNT")"
done
return "$failed"
}
rf_do_remove() {
local id="${1:-}"
if [[ -z $id || ! -e $(rf_ws_file "$id") ]]; then
rf_bad "$(rf_msg "Which workspace? See \`%s list\` for the names." "$RF_NAME")"
return 1
fi
rf_ws_load "$id"
rf_ws_purge "$id"
rf_ws_ids
(( ${#RF_WS_IDS[@]} == 0 )) && { rf_timer_set no || true; }
rf_ok "$(rf_msg "%s and its apps are removed." "$WS_NAME")"
}
rf_do_list() {
local id i
rf_ws_ids
if (( ${#RF_WS_IDS[@]} == 0 )); then
rf_note "$(rf_msg "No workspace yet. Add one with \`%s add\`." "$RF_NAME")"
return 0
fi
for id in "${RF_WS_IDS[@]}"; do
rf_ws_load "$id"
rf_ws_apps_load "$id"
printf '%s%s%s %s(%s, %s)%s\n' "$RF_C_BOLD" "$WS_NAME" "$RF_C_RESET" "$RF_C_DIM" "$id" "$WS_ACCOUNT" "$RF_C_RESET"
for i in "${!RF_APP_SLUGS[@]}"; do
printf ' %-36s %s\n' "$id/${RF_APP_SLUGS[i]}" "${RF_APP_TITLES[i]}"
done
done
}
# rf_do_run <app or workspace/app> [file]
rf_do_run() {
local spec="${1:-}" file="${2:-}" id slug found=() i
if [[ -z $spec ]]; then
rf_bad "$(rf_msg "Which app? See \`%s list\`." "$RF_NAME")"
return 1
fi
if [[ $spec == */* ]]; then
rf_launch "${spec%%/*}" "${spec#*/}" "$file"
return
fi
rf_ws_ids
for id in "${RF_WS_IDS[@]}"; do
rf_ws_apps_load "$id"
for i in "${!RF_APP_SLUGS[@]}"; do
[[ ${RF_APP_SLUGS[i]} == "$spec" ]] && found+=("$id")
done
done
case "${#found[@]}" in
0) rf_bad "$(rf_msg "There is no app %s. See \`%s list\`." "$spec" "$RF_NAME")"; return 1 ;;
1) slug="$spec"; rf_launch "${found[0]}" "$slug" "$file" ;;
*) rf_bad "$(rf_msg "Several workspaces have %s. Write it as WORKSPACE/APP, see \`%s list\`." "$spec" "$RF_NAME")"; return 1 ;;
esac
}
rf_do_help() {
cat <<- EOF
$RF_PRETTY $RF_VERSION
$(rf_msg "Usage: rdfeed [command]")
$(rf_msg "Commands:")
add [ADDRESS] $(rf_msg "Add the workspace of your company")
refresh [NAME] $(rf_msg "Load the apps again")
list $(rf_msg "List the workspaces and their apps")
run APP [FILE] $(rf_msg "Start an app, or open a file with it")
remove NAME $(rf_msg "Remove a workspace and its apps")
-h, --help $(rf_msg "Show this help")
-V, --version $(rf_msg "Show the version")
$(rf_msg "Without a command an interactive menu is shown.")
EOF
}
# ---------------------------------------------------------------------------
# Dispatch
# ---------------------------------------------------------------------------
main() {
local cmd="${1:-}"
[[ $# -gt 0 ]] && shift
case "$cmd" in
add|subscribe) rf_do_add "$@" ;;
refresh|sync) rf_do_refresh "$@" ;;
list|ls) rf_do_list ;;
run|start) rf_do_run "$@" ;;
remove|delete) rf_do_remove "$@" ;;
-h|--help|help) rf_do_help ;;
-V|--version) printf '%s %s\n' "$RF_NAME" "$RF_VERSION" ;;
'') rf_ui_menu ;;
*)
rf_bad "$(rf_msg "Unknown command: %s" "$cmd")"
printf '\n'
rf_do_help
exit 1
;;
esac
}
main "$@"
+212
View File
@@ -0,0 +1,212 @@
#!/usr/bin/env python3
#
# A fake RD Web Access server for the tests: the feed behind the NTLM login,
# the forms ticket it hands out, .rdp files and icons. It checks the NTLMv2
# answer for real, against one user and password.
#
# python3 tests/fake_rdweb.py [port] serve until Ctrl+C
#
# Copyright (C) 2026 Felitendo
# SPDX-License-Identifier: GPL-3.0-or-later
import base64
import hmac
import http.server
import os
import socketserver
import struct
import sys
import threading
import urllib.parse
import importlib.machinery # noqa: E402
import importlib.util # noqa: E402
# rdfeed-fetch has no .py ending, so it is loaded by path.
_loader = importlib.machinery.SourceFileLoader(
"rdfeed_fetch", os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "src", "fetch", "rdfeed-fetch"))
_spec = importlib.util.spec_from_loader("rdfeed_fetch", _loader)
fetch = importlib.util.module_from_spec(_spec)
_loader.exec_module(fetch)
DOMAIN = "CONTOSO"
USER = "jdoe"
PASSWORD = "Correct horse 1"
TOKEN = "5EC2E7F00DBA11ADE0F7EED5ACC0FFEE"
APPS = [
("winword-RemoteApps-CmsRdsh", "Word", "RemoteApp"),
("excel-RemoteApps-CmsRdsh", "Excel", "RemoteApp"),
("powerpnt-RemoteApps-CmsRdsh", "PowerPoint", "RemoteApp"),
("desktop-RemoteApps-CmsRdsh", "Contoso Desktop", "Desktop"),
]
def extensions(alias):
"""Excel comes with its file types, as an administrator would enter them."""
if not alias.startswith("excel"):
return "<FileExtensions />"
return ('<FileExtensions><FileExtension Name=".xlsx" PrimaryHandler="True" />'
'<FileExtension Name=".CSV" PrimaryHandler="False" /></FileExtensions>')
def feed_xml():
resources = ""
for alias, title, kind in APPS:
resources += f"""
<Resource ID="{alias}" Alias="{alias}" Title="{title}" LastUpdated="2026-10-01T08:00:00Z" Type="{kind}" ShowByDefault="True">
<Icons>
<IconRaw FileType="Ico" FileURL="/RDWeb/Pages/rdp/{alias}.ico" />
<Icon32 Dimensions="32x32" FileType="Png" FileURL="/RDWeb/Pages/rdp/{alias}.png" />
</Icons>
{extensions(alias)}
<Folders><Folder Name="/" /></Folders>
<HostingTerminalServers>
<HostingTerminalServer>
<ResourceFile FileExtension=".rdp" URL="/RDWeb/Pages/rdp/cpub-{alias}.rdp" />
<TerminalServerRef Ref="RDSH01.contoso.test" />
</HostingTerminalServer>
</HostingTerminalServers>
</Resource>"""
return f"""<?xml version="1.0" encoding="utf-8"?>
<ResourceCollection PubDate="2026-10-01T08:00:00Z" SchemaVersion="2.1" xmlns="http://schemas.microsoft.com/ts/2007/05/tswf">
<Publisher LastUpdated="2026-10-01T08:00:00Z" Name="Contoso Apps" ID="RDSH01.contoso.test" Description="">
<Resources>{resources}
</Resources>
</Publisher>
</ResourceCollection>
""".encode()
def rdp_file(alias, title, kind):
lines = [
"full address:s:RDBROKER.contoso.test",
"gatewayhostname:s:gateway.contoso.test",
"gatewayusagemethod:i:1",
"promptcredentialonce:i:0",
"prompt for credentials on client:i:1",
"drivestoredirect:s:*",
"devicestoredirect:s:*",
"camerastoredirect:s:*",
"loadbalanceinfo:s:tsv://MS Terminal Services Plugin.1.RemoteApps",
]
if kind == "RemoteApp":
lines += ["remoteapplicationmode:i:1", f"remoteapplicationprogram:s:||{alias.split('-')[0]}",
f"remoteapplicationname:s:{title}"]
return ("\r\n".join(lines) + "\r\n").encode("utf-16")
def ico_32bpp(size=48, rgba=(30, 110, 220, 255)):
"""An .ico with one 32-bit bitmap frame, the way Windows writes them."""
r, g, b, a = rgba
header = struct.pack("<IiiHHIIiiII", 40, size, size * 2, 1, 32, 0, 0, 0, 0, 0, 0)
pixels = bytes((b, g, r, a)) * (size * size)
mask = b"\x00" * (((size + 31) // 32) * 4 * size)
dib = header + pixels + mask
return (struct.pack("<HHH", 0, 1, 1)
+ struct.pack("<BBBBHHII", size, size, 0, 0, 1, 32, len(dib), 22) + dib)
def ntlm_challenge():
def av(kind, value):
data = value.encode("utf-16-le")
return struct.pack("<HH", kind, len(data)) + data
info = (av(2, DOMAIN) + av(1, "RDWEB01") + av(4, "contoso.test") + av(3, "rdweb01.contoso.test")
+ struct.pack("<HH", 7, 8) + struct.pack("<Q", 133000000000000000) + struct.pack("<HH", 0, 0))
server_challenge = os.urandom(8)
target = DOMAIN.encode("utf-16-le")
head = 56
message = (b"NTLMSSP\x00" + struct.pack("<I", 2) + struct.pack("<HHI", len(target), len(target), head)
+ struct.pack("<I", 0xE2898215) + server_challenge + b"\x00" * 8
+ struct.pack("<HHI", len(info), len(info), head + len(target)) + fetch.NTLM_VERSION
+ target + info)
return message, server_challenge
def ntlm_check(message, server_challenge):
"""True if the AUTHENTICATE message proves the password."""
def field(i):
length, _, offset = struct.unpack("<HHI", message[12 + 8 * i:20 + 8 * i])
return message[offset:offset + length]
nt, domain, user = field(1), field(2).decode("utf-16-le"), field(3).decode("utf-16-le")
if user.lower() != USER or domain.upper() != DOMAIN or len(nt) < 32:
return False
key = fetch.ntowfv2(user, domain, PASSWORD)
proof = hmac.new(key, server_challenge + nt[16:], "md5").digest()
return hmac.compare_digest(proof, nt[:16])
class Handler(http.server.BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def log_message(self, *args):
pass
def reply(self, status, body=b"", headers=None):
self.send_response(status)
for k, v in (headers or {}).items():
if isinstance(v, list):
for one in v:
self.send_header(k, one)
else:
self.send_header(k, v)
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def signed_in(self):
cookies = self.headers.get("Cookie", "")
return f"TSWAFeedAuthCookie={TOKEN}" in cookies
def do_GET(self):
path = urllib.parse.urlsplit(self.path).path
lower = path.lower()
if lower == "/rdweb/feed/webfeed.aspx":
if self.signed_in():
return self.reply(200, feed_xml(), {"Content-Type": "application/x-msts-radc+xml; charset=utf-8"})
return self.reply(302, b"", {"Location": "/RDWeb/FeedLogin/WebFeedLogin.aspx"})
if lower == "/rdweb/feedlogin/webfeedlogin.aspx":
auth = self.headers.get("Authorization", "")
if auth.startswith("NTLM "):
message = base64.b64decode(auth[5:])
kind = struct.unpack("<I", message[8:12])[0]
if kind == 1:
challenge, self.server_challenge = ntlm_challenge()
return self.reply(401, b"", {"WWW-Authenticate": "NTLM " + base64.b64encode(challenge).decode()})
if kind == 3 and ntlm_check(message, getattr(self, "server_challenge", b"")):
return self.reply(200, TOKEN.encode(), {"Content-Type": "application/x-msts-webfeed-login; charset=utf-8"})
return self.reply(401, b"denied", {"WWW-Authenticate": ["NTLM", "Negotiate"], "Content-Type": "text/html"})
if lower.startswith("/rdweb/pages/rdp/"):
if not self.signed_in():
return self.reply(302, b"", {"Location": "/RDWeb/Pages/en-US/login.aspx?ReturnUrl=" + path})
name = path.rsplit("/", 1)[1]
for alias, title, kind in APPS:
if name == f"cpub-{alias}.rdp":
return self.reply(200, rdp_file(alias, title, kind), {"Content-Type": "application/x-rdp"})
if name == f"{alias}.ico":
return self.reply(200, ico_32bpp(), {"Content-Type": "image/x-icon"})
if name == f"{alias}.png":
return self.reply(404)
return self.reply(404, b"<html>not here</html>", {"Content-Type": "text/html"})
class Server(socketserver.ThreadingMixIn, http.server.HTTPServer):
daemon_threads = True
allow_reuse_address = True
def serve(port=0):
"""Starts the server in a thread. Returns it; its URL base is server.base."""
server = Server(("127.0.0.1", port), Handler)
server.base = f"http://127.0.0.1:{server.server_address[1]}"
threading.Thread(target=server.serve_forever, daemon=True).start()
return server
if __name__ == "__main__":
srv = serve(int(sys.argv[1]) if len(sys.argv) > 1 else 0)
print(srv.base + "/RDWeb/Feed/webfeed.aspx", flush=True)
try:
threading.Event().wait()
except KeyboardInterrupt:
pass
+163
View File
@@ -0,0 +1,163 @@
#!/usr/bin/env bash
#
# The rdfeed command from start to end, against tests/fake_rdweb.py: add a
# workspace, list, refresh, start an app, remove it again.
#
# Everything happens in a temporary home. secret-tool, systemctl, notify-send,
# kbuildsycoca6 and xfreerdp3 are fakes on PATH, so the real keyring, timers,
# notifications and app menu stay untouched.
#
# Copyright (C) 2026 Felitendo
# SPDX-License-Identifier: GPL-3.0-or-later
set -uo pipefail
root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
tmp="$(mktemp -d)"
server=''
trap '[[ -n $server ]] && kill "$server" 2>/dev/null; rm -rf -- "$tmp"' EXIT
failed=0
export HOME="$tmp/home" XDG_CONFIG_HOME="$tmp/config" XDG_DATA_HOME="$tmp/data" XDG_CACHE_HOME="$tmp/cache"
export RF_LIBDIR="$root/src/lib" RF_FETCH="$root/src/fetch/rdfeed-fetch" RF_LOCALEDIR="$tmp/locale"
RF_HOOK="$(readlink -f "${RDFEED_HOOK:-$root/build/rdfeed-hook.so}")"
export RF_HOOK
export LANG=C LC_ALL=C NO_COLOR=1 XDG_CURRENT_DESKTOP=test XDG_RUNTIME_DIR="$tmp/run"
mkdir -p "$HOME" "$tmp/bin"
# The fakes. Each writes what it was asked into $tmp, as a log or a file.
cat > "$tmp/bin/secret-tool" <<- 'EOF'
#!/usr/bin/env bash
store="${0%/bin/*}/secrets"; mkdir -p "$store"
cmd="$1"; shift; ws=''
while (( $# )); do [[ $1 == workspace ]] && ws="$2"; shift; done
case "$cmd" in
store) cat > "$store/$ws" ;;
lookup) [[ -f $store/$ws ]] && cat "$store/$ws" ;;
clear) rm -f "$store/$ws" ;;
esac
EOF
cat > "$tmp/bin/xfreerdp3" <<- 'EOF'
#!/usr/bin/env bash
out="${0%/bin/*}"
cat > "$out/freerdp-args"
cp "$(head -n1 "$out/freerdp-args")" "$out/freerdp-rdp"
env > "$out/freerdp-env"
echo "[INFO] connected"
echo "[rdfeed] gateway-delay 12" >&2
exit "${FAKE_FREERDP_EXIT:-0}"
EOF
for fake in systemctl notify-send kbuildsycoca6 update-desktop-database; do
# shellcheck disable=SC2016 # the script expands when it runs, not here
printf '#!/usr/bin/env bash\necho "%s $*" >> "${0%%/bin/*}/calls"\n[[ $1 == --user && $2 == cat ]] && exit 1\nexit 0\n' "$fake" > "$tmp/bin/$fake"
done
chmod +x "$tmp/bin"/*
export PATH="$tmp/bin:$PATH"
rdfeed() { bash "$root/src/rdfeed" "$@"; }
check() {
if eval "$2"; then
printf 'ok %s\n' "$1"
else
printf 'FAIL %s\n' "$1"
failed=1
fi
}
# The URL helpers, on their own.
url_helpers() (
source "$RF_LIBDIR/common.sh"; source "$RF_LIBDIR/config.sh"; source "$RF_LIBDIR/workspace.sh"
for pair in "rdweb.contoso.com|https://rdweb.contoso.com/RDWeb/Feed/webfeed.aspx" \
"office.contoso.com/rdweb/feed/webfeed|https://office.contoso.com/rdweb/feed/webfeed.aspx" \
"https://x.test/RDWeb/Pages/en-US/Default.aspx|https://x.test/RDWeb/Feed/webfeed.aspx" \
"HTTPS://x.test:8443/RDWeb/|https://x.test:8443/RDWeb/Feed/webfeed.aspx" \
"https://x.test/custom/feed.aspx|https://x.test/custom/feed.aspx"; do
rf_feed_url "${pair%%|*}"
[[ $RF_URL == "${pair#*|}" ]] || { echo " $RF_URL != ${pair#*|}"; exit 1; }
done
rf_split_user 'CONTOSO\jdoe'; [[ $RF_USER == jdoe && $RF_DOMAIN == CONTOSO ]] || exit 1
rf_split_user 'jdoe@contoso.com'; [[ $RF_USER == jdoe@contoso.com && -z $RF_DOMAIN ]] || exit 1
rf_split_user 'jdoe'; [[ $RF_USER == jdoe && $RF_DOMAIN == - ]] || exit 1
[[ $(rf_ws_id_for "https://Office.Contoso.COM/RDWeb/Feed/webfeed.aspx") == office-contoso-com ]] || exit 1
)
if url_helpers; then check "addresses, user names and ids" true; else check "addresses, user names and ids" false; fi
python3 "$root/tests/fake_rdweb.py" > "$tmp/url" &
server=$!
for _ in $(seq 50); do [[ -s $tmp/url ]] && break; sleep 0.1; done
url="$(< "$tmp/url")"
id=127-0-0-1
password="$(python3 -c 'import sys; sys.path.insert(0, sys.argv[1]); import fake_rdweb; print(fake_rdweb.PASSWORD)' "$root/tests")"
out="$(printf 'CONTOSO\\jdoe\nwrong\n' | rdfeed add "$url" 2>&1)"
rc=$?
check "a wrong password is refused" "[[ \$rc -ne 0 ]] && grep -q 'did not accept' <<< \"\$out\""
check "and leaves nothing behind" "[[ ! -e $XDG_CONFIG_HOME/rdfeed/workspaces/$id ]]"
out="$(printf 'jdoe\n%s\n' "$password" | rdfeed add "${url%/RDWeb/*}" 2>&1)"
check "add signs in and loads the apps" "grep -q '4 apps from Contoso Apps are now in your app menu' <<< \"\$out\""
ws="$XDG_CONFIG_HOME/rdfeed/workspaces/$id"
check "the workspace remembers the server's domain" "grep -qx 'Domain=CONTOSO' '$ws' && grep -qx 'User=jdoe' '$ws'"
check "the password is in the keyring, as typed" "[[ \$(< '$tmp/secrets/$id') == \"\$password\" ]]"
entry="$XDG_DATA_HOME/applications/rdfeed-$id--powerpnt.desktop"
check "each app has a menu entry" "[[ \$(ls '$XDG_DATA_HOME/applications' | wc -l) -eq 4 ]]"
check "which starts it" "grep -q 'Exec=$root/src/rdfeed run $id/powerpnt' '$entry'"
check "with its own icon" "grep -qx 'Icon=$XDG_DATA_HOME/rdfeed/$id/icons/powerpnt.png' '$entry'"
word="$XDG_DATA_HOME/applications/rdfeed-$id--winword.desktop"
if [[ -r /usr/share/mime/globs2 ]]; then
check "Word is offered for .docx files" "grep -q '^MimeType=.*application/vnd.openxmlformats-officedocument.wordprocessingml.document;' '$word' && grep -q 'run $id/winword %f\$' '$word'"
check "Excel for the types of the feed" "grep -qx 'MimeType=.*text/csv;.*' '$XDG_DATA_HOME/applications/rdfeed-$id--excel.desktop' || grep -q 'MimeType=.*csv' '$XDG_DATA_HOME/applications/rdfeed-$id--excel.desktop'"
fi
check "the desktop for none" "! grep -q MimeType '$XDG_DATA_HOME/applications/rdfeed-$id--desktop.desktop'"
if command -v desktop-file-validate > /dev/null; then
check "the entries are valid" "desktop-file-validate --no-hints '$XDG_DATA_HOME'/applications/*.desktop"
fi
check "the workspace has a folder" "grep -q '<Category>X-rdfeed-$id</Category>' '$XDG_CONFIG_HOME/menus/applications-merged/rdfeed-$id.menu' && grep -qx 'Name=Contoso Apps' '$XDG_DATA_HOME/desktop-directories/rdfeed-$id.directory'"
out="$(rdfeed list 2>&1)"
check "list shows the apps" "grep -q '$id/powerpnt *PowerPoint' <<< \"\$out\""
out="$(rdfeed refresh < /dev/null 2>&1)"
check "refresh takes the password from the keyring" "grep -q 'Contoso Apps: 4 apps' <<< \"\$out\""
rdfeed run powerpnt > /dev/null 2>&1
check "run hands FreeRDP the .rdp file first" "[[ \$(head -n1 '$tmp/freerdp-args') == '$XDG_DATA_HOME/rdfeed/$id/rdp/powerpnt.rdp' ]]"
check "with the user, domain and password" "grep -qx '/u:jdoe' '$tmp/freerdp-args' && grep -qx '/d:CONTOSO' '$tmp/freerdp-args' && grep -qxF \"/p:\$password\" '$tmp/freerdp-args'"
check "and the hook, with GFX left on" "grep -qx 'LD_PRELOAD=$RF_HOOK' '$tmp/freerdp-env' && ! grep -q 'RDFEED_NO_GFX' '$tmp/freerdp-env'"
check "the log has what FreeRDP said" "grep -q 'gateway-delay 12' '$XDG_CACHE_HOME/rdfeed/$id--powerpnt.log'"
check "a notification says a second prompt comes" "grep -q 'notify-send .*A second sign-in is coming' '$tmp/calls'"
mkdir -p "$HOME/Downloads" "$HOME/Documents/Reports"
: > "$HOME/Downloads/Report, final.docx"
rdfeed run "$id/winword" "$HOME/Downloads/Report, final.docx" > /dev/null 2>&1
check "a file opens through its shared folder" "grep -qxF 'remoteapplicationcmdline:s:\"\\\\tsclient\\Downloads\\Report, final.docx\"'\$'\\r' '$tmp/freerdp-rdp' && grep -qx '/drive:Downloads,$HOME/Downloads' '$tmp/freerdp-args'"
check "and its copy of the .rdp is gone after" "[[ -z \$(ls -A '$XDG_RUNTIME_DIR/rdfeed' 2>/dev/null) ]]"
check "and a notification names it" "grep -q 'notify-send .*Opening Report, final.docx in Word' '$tmp/calls'"
rdfeed run "$id/winword" "$HOME/nothing.docx" > /dev/null 2>&1
rc=$?
check "a missing file is refused" "[[ \$rc -ne 0 ]]"
# shellcheck disable=SC2016 # user-dirs.dirs wants $HOME as it is
printf 'XDG_DOCUMENTS_DIR="$HOME/Documents"\n' > "$XDG_CONFIG_HOME/user-dirs.dirs"
printf 'Share=documents\nGraphics=classic\nSound=no\nOpenWith=no\n' > "$XDG_CONFIG_HOME/rdfeed/config"
: > "$HOME/Documents/Reports/q3.xlsx"
rdfeed run "$id/excel" "$HOME/Documents/Reports/q3.xlsx" > /dev/null 2>&1
check "a file in the shared folder needs no drive of its own" "grep -qF 'tsclient\\Documents\\Reports\\q3.xlsx' '$tmp/freerdp-rdp' && [[ \$(grep -c '^/drive:' '$tmp/freerdp-args') -eq 1 ]]"
check "the settings reach FreeRDP" "grep -qx 'RDFEED_NO_GFX=1' '$tmp/freerdp-env' && ! grep -qx '/sound' '$tmp/freerdp-args'"
rdfeed refresh < /dev/null > /dev/null 2>&1
check "with Open with off the entries name no file types" "! grep -q MimeType '$word' && grep -q 'run $id/winword\$' '$word'"
FAKE_FREERDP_EXIT=131 rdfeed run "$id/powerpnt" > /dev/null 2>&1
rc=$?
check "a failed start says so" "[[ \$rc -ne 0 ]] && grep -q 'notify-send .*PowerPoint could not start' '$tmp/calls'"
rdfeed run nothing > /dev/null 2>&1
rc=$?
check "an unknown app is refused" "[[ \$rc -ne 0 ]]"
rdfeed remove "$id" > /dev/null 2>&1
check "remove takes everything away" "[[ ! -e '$ws' && ! -e '$tmp/secrets/$id' && ! -d '$XDG_DATA_HOME/rdfeed/$id' ]] && ! ls '$XDG_DATA_HOME'/applications/rdfeed-* > /dev/null 2>&1"
exit "$failed"
+220
View File
@@ -0,0 +1,220 @@
#!/usr/bin/env python3
#
# rdfeed-fetch: NTLM against the test vectors of MS-NLMP, the feed, the .rdp
# files, the icons, and whole subscriptions against tests/fake_rdweb.py.
#
# Copyright (C) 2026 Felitendo
# SPDX-License-Identifier: GPL-3.0-or-later
import hmac
import os
import struct
import subprocess
import sys
import tempfile
import unittest
import zlib
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import fake_rdweb # noqa: E402
fetch = fake_rdweb.fetch
FETCH = os.path.join(HERE, "..", "src", "fetch", "rdfeed-fetch")
def png_info(png):
"""Width, height and the RGBA bytes of a PNG written by png_encode."""
assert png[:8] == fetch.PNG_SIGNATURE
width, height = struct.unpack(">II", png[16:24])
pos, idat = 8, b""
while pos < len(png):
length = struct.unpack(">I", png[pos:pos + 4])[0]
kind = png[pos + 4:pos + 8]
if kind == b"IDAT":
idat += png[pos + 8:pos + 8 + length]
pos += 12 + length
raw = zlib.decompress(idat)
return width, height, raw
class Ntlm(unittest.TestCase):
def test_md4(self):
self.assertEqual(fetch.md4(b"").hex(), "31d6cfe0d16ae931b73c59d7e0c089c0")
self.assertEqual(fetch.md4(b"abc").hex(), "a448017aaf21d8525fc10ae87aa6729d")
self.assertEqual(fetch.md4(b"1234567890" * 8).hex(), "e33b4ddc9c38f2199c3e7b164fcc0536")
# MS-NLMP 4.2.4: User, Domain, Password, server challenge 0123456789abcdef,
# client challenge aa..aa, time 0, target info Domain and Server.
def test_ntlmv2_vectors(self):
key = fetch.ntowfv2("User", "Domain", "Password")
self.assertEqual(key.hex(), "0c868a403bfd7a93a3001ef22ef02e3f")
server, client = bytes.fromhex("0123456789abcdef"), b"\xaa" * 8
lm = hmac.new(key, server + client, "md5").digest() + client
self.assertEqual(lm.hex(), "86c35097ac9cec102554764a57cccc19aaaaaaaaaaaaaaaa")
info = (struct.pack("<HH", 2, 12) + "Domain".encode("utf-16-le")
+ struct.pack("<HH", 1, 12) + "Server".encode("utf-16-le") + b"\x00" * 4)
nt = fetch.ntlmv2_response(key, server, client, b"\x00" * 8, info)
self.assertEqual(nt[:16].hex(), "68cd0ab851e51c96aabc927bebef6a1c")
def test_domain_from_server(self):
challenge, server_challenge = fake_rdweb.ntlm_challenge()
message, domain = fetch.ntlm_authenticate(challenge, "jdoe", None, fake_rdweb.PASSWORD)
self.assertEqual(domain, "CONTOSO")
self.assertTrue(fake_rdweb.ntlm_check(message, server_challenge))
message, _ = fetch.ntlm_authenticate(challenge, "jdoe", None, "wrong")
self.assertFalse(fake_rdweb.ntlm_check(message, server_challenge))
class Feed(unittest.TestCase):
def test_parse(self):
name, apps = fetch.parse_feed(fake_rdweb.feed_xml())
self.assertEqual(name, "Contoso Apps")
self.assertEqual([a["slug"] for a in apps], ["winword", "excel", "powerpnt", "desktop"])
self.assertEqual([a["type"] for a in apps], ["RemoteApp"] * 3 + ["Desktop"])
self.assertEqual(apps[0]["rdp"], "/RDWeb/Pages/rdp/cpub-winword-RemoteApps-CmsRdsh.rdp")
self.assertEqual(apps[1]["types"], ["xlsx", "csv"])
self.assertEqual(apps[0]["types"], [])
def test_known_types(self):
self.assertEqual(fetch.known_types("||winword", "Word", "winword-x")[:2], ["docx", "docm"])
self.assertEqual(fetch.known_types("||powerpoint", "PowerPoint", ""), "pptx pptm ppt potx potm ppsx pps odp".split())
self.assertEqual(fetch.known_types("||trueview", "DWG TrueView", "trueview-x"), ["dwg", "dxf"])
self.assertEqual(fetch.known_types("||pdf24", "PDF24", ""), ["pdf"])
self.assertEqual(fetch.known_types("||wordpad", "WordPad", "wordpad-x"), [])
self.assertEqual(fetch.known_types("||pwchange", "Passwort ändern", "pwchange-x"), [])
def test_not_a_feed(self):
for data in (b"<html><body>login</body></html>", b"", b"<RDWAPage/>"):
with self.assertRaises(fetch.FeedError):
fetch.parse_feed(data)
def test_slugs(self):
taken = set()
self.assertEqual(fetch.make_slug("Passwort ändern", taken), "passwort-andern")
self.assertEqual(fetch.make_slug("Passwort ändern", taken), "passwort-andern-2")
self.assertEqual(fetch.make_slug("计算器", taken), "app")
self.assertEqual(fetch.make_slug("!!!", taken), "app-2")
self.assertEqual(fetch.common_suffix(["a-X-Y", "bb-X-Y"]), "-X-Y")
self.assertEqual(fetch.common_suffix(["word", "sword"]), "")
self.assertEqual(fetch.common_suffix(["only-one"]), "")
class Rdp(unittest.TestCase):
def test_sanitize(self):
text = fetch.decode_rdp(fake_rdweb.rdp_file("winword-x", "Word", "RemoteApp"))
out = fetch.sanitize_rdp(text)
self.assertIn("promptcredentialonce:i:1\r\n", out)
self.assertIn("prompt for credentials on client:i:0\r\n", out)
for gone in ("drivestoredirect", "devicestoredirect", "camerastoredirect"):
self.assertNotIn(gone, out)
self.assertIn("remoteapplicationprogram:s:||winword\r\n", out)
def test_encodings(self):
line = "full address:s:host\r\n"
for data in (line.encode("utf-16"), line.encode("utf-16-le"), line.encode("utf-8-sig"), line.encode()):
self.assertEqual(fetch.decode_rdp(data), line)
def test_append(self):
self.assertEqual(fetch.rdp_set("a:s:b\r\n", "promptcredentialonce", "i", "1"),
"a:s:b\r\npromptcredentialonce:i:1\r\n")
class Icons(unittest.TestCase):
def test_32bpp(self):
width, height, raw = png_info(fetch.ico_to_png(fake_rdweb.ico_32bpp(48, (30, 110, 220, 128))))
self.assertEqual((width, height), (48, 48))
self.assertEqual(raw[1:5], bytes((30, 110, 220, 128)))
def test_24bpp_with_mask(self):
size = 16
header = struct.pack("<IiiHHIIiiII", 40, size, size * 2, 1, 24, 0, 0, 0, 0, 0, 0)
stride = ((size * 24 + 31) // 32) * 4
pixels = (bytes((0, 0, 255)) * size + b"\x00" * (stride - size * 3)) * size
# The top row of the image (the last one in the file) is transparent.
mask = b"\x00\x00\x00\x00" * (size - 1) + b"\xff\xff\x00\x00"
dib = header + pixels + mask
ico = (struct.pack("<HHH", 0, 1, 1)
+ struct.pack("<BBBBHHII", size, size, 0, 0, 1, 24, len(dib), 22) + dib)
width, height, raw = png_info(fetch.ico_to_png(ico))
self.assertEqual((width, height), (16, 16))
self.assertEqual(raw[1:5], bytes((255, 0, 0, 0)))
row = 1 + width * 4
self.assertEqual(raw[row + 1:row + 5], bytes((255, 0, 0, 255)))
def test_png_frame_and_biggest(self):
small = fake_rdweb.ico_32bpp(16)
png = fetch.png_encode(2, 1, [bytes((1, 2, 3, 4, 5, 6, 7, 8))])
# Two entries: a 16 px bitmap and a 256 px PNG (width 0 means 256).
dib = small[22:]
ico = (struct.pack("<HHH", 0, 1, 2)
+ struct.pack("<BBBBHHII", 16, 16, 0, 0, 1, 32, len(dib), 38)
+ struct.pack("<BBBBHHII", 0, 0, 0, 0, 1, 32, len(png), 38 + len(dib)) + dib + png)
self.assertEqual(fetch.ico_to_png(ico), png)
def test_garbage(self):
self.assertIsNone(fetch.ico_to_png(b"not an icon"))
self.assertIsNone(fetch.ico_to_png(b"\x00\x00\x01\x00\x01\x00" + b"\x00" * 16))
class Subscribe(unittest.TestCase):
"""rdfeed-fetch against the fake server, as rdfeed runs it."""
@classmethod
def setUpClass(cls):
cls.server = fake_rdweb.serve()
cls.url = cls.server.base + "/RDWeb/Feed/webfeed.aspx"
@classmethod
def tearDownClass(cls):
cls.server.shutdown()
def run_fetch(self, password, url=None, user="jdoe", domain=None):
out = tempfile.mkdtemp()
args = [sys.executable, FETCH, "--url", url or self.url, "--user", user, "--out", out, "--timeout", "5"]
if domain is not None:
args += ["--domain", domain]
proc = subprocess.run(args, input=password + "\n", capture_output=True, text=True, timeout=60)
return proc, out
def test_ok(self):
proc, out = self.run_fetch(fake_rdweb.PASSWORD)
self.assertEqual(proc.returncode, 0, proc.stderr)
lines = [line.split("\t") for line in proc.stdout.splitlines()]
self.assertIn(["workspace", "Contoso Apps"], lines)
self.assertIn(["domain", "CONTOSO"], lines)
self.assertIn(["app", "powerpnt", "RemoteApp", "PowerPoint"], lines)
self.assertIn(["app", "desktop", "Desktop", "Contoso Desktop"], lines)
with open(os.path.join(out, "apps.tsv")) as f:
rows = [line.split("\t") for line in f.read().splitlines()]
self.assertEqual(len(rows), 4)
types = {row[0]: row[3] for row in rows}
self.assertTrue(types["winword"].startswith("docx docm doc "))
self.assertEqual(types["excel"], "xlsx csv")
self.assertEqual(types["desktop"], "")
with open(os.path.join(out, "rdp", "winword.rdp")) as f:
rdp = f.read()
self.assertIn("promptcredentialonce:i:1", rdp)
self.assertNotIn("drivestoredirect", rdp)
with open(os.path.join(out, "icons", "excel.png"), "rb") as f:
self.assertEqual(png_info(f.read())[:2], (48, 48))
def test_domain_given(self):
proc, _ = self.run_fetch(fake_rdweb.PASSWORD, domain="contoso")
self.assertEqual(proc.returncode, 0, proc.stderr)
def test_wrong_password(self):
proc, _ = self.run_fetch("nope")
self.assertEqual(proc.returncode, fetch.EXIT_AUTH)
def test_no_feed(self):
proc, _ = self.run_fetch(fake_rdweb.PASSWORD, url=self.server.base + "/somewhere/else")
self.assertEqual(proc.returncode, fetch.EXIT_NOFEED)
def test_unreachable(self):
proc, _ = self.run_fetch(fake_rdweb.PASSWORD, url="http://127.0.0.1:9/RDWeb/Feed/webfeed.aspx")
self.assertEqual(proc.returncode, fetch.EXIT_NETWORK)
if __name__ == "__main__":
unittest.main()
+88
View File
@@ -0,0 +1,88 @@
#!/usr/bin/env bash
#
# rdfeed-hook.so: the wait before a new gateway tunnel, and GFX kept off.
# Connections go to port 443 of this computer, nothing has to answer there.
#
# Copyright (C) 2026 Felitendo
# SPDX-License-Identifier: GPL-3.0-or-later
set -uo pipefail
cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." || exit 1
hook="$(readlink -f "${RDFEED_HOOK:-build/rdfeed-hook.so}")"
[[ -f $hook ]] || { echo "no hook at $hook, run make first" >&2; exit 1; }
failed=0
check() {
if [[ $2 == "$3" ]]; then
printf 'ok %s\n' "$1"
else
printf 'FAIL %s: got "%s", want "%s"\n' "$1" "$2" "$3"
failed=1
fi
}
# timings <wait mode> <pause>
# One connection, the pause, then two right after each other. Prints how long
# each one waited, in whole seconds, and whether the hook said so.
timings() {
LD_PRELOAD="$hook" RDFEED_GW_DELAY=1 RDFEED_GW_WAIT="$1" python3 - "$2" 2>&1 <<- 'EOF'
import socket, sys, time
def attempt():
start = time.monotonic()
s = socket.socket()
try:
s.connect(("127.0.0.1", 443))
except OSError:
pass
s.close()
return round(time.monotonic() - start)
first = attempt()
time.sleep(float(sys.argv[1]))
second = attempt()
third = attempt()
print(first, second, third)
EOF
}
check "auto waits after a long first sign-in" "$(timings auto 3 | tr '\n' ' ')" "[rdfeed] gateway-delay 1 0 1 0 "
check "auto does not wait after a quick one" "$(timings auto 2.1)" "0 0 0"
check "always waits" "$(timings always 2.1 | tail -n1)" "0 1 0"
check "never waits not" "$(timings never 3)" "0 0 0"
check "the same tunnel does not wait" "$(timings always 0.5)" "0 0 0"
# GFX: what FreeRDP's own command line parser makes of it, with and without.
gfx() {
env "$@" python3 - <<- 'EOF'
import ctypes
try:
core = ctypes.CDLL("libfreerdp3.so.3", mode=ctypes.RTLD_GLOBAL)
client = ctypes.CDLL("libfreerdp-client3.so.3", mode=ctypes.RTLD_GLOBAL)
except OSError:
print("skip")
raise SystemExit
core.freerdp_settings_new.restype = ctypes.c_void_p
core.freerdp_settings_new.argtypes = [ctypes.c_uint32]
core.freerdp_settings_get_key_for_name.restype = ctypes.c_ssize_t
core.freerdp_settings_get_key_for_name.argtypes = [ctypes.c_char_p]
core.freerdp_settings_get_bool.argtypes = [ctypes.c_void_p, ctypes.c_size_t]
client.freerdp_client_settings_parse_command_line.argtypes = [
ctypes.c_void_p, ctypes.c_int, ctypes.POINTER(ctypes.c_char_p), ctypes.c_int]
settings = core.freerdp_settings_new(0)
argv = [b"xfreerdp3", b"/v:host.invalid", b"/gfx"]
client.freerdp_client_settings_parse_command_line(
settings, len(argv), (ctypes.c_char_p * len(argv))(*argv), 0)
key = core.freerdp_settings_get_key_for_name(b"FreeRDP_SupportGraphicsPipeline")
print("on" if core.freerdp_settings_get_bool(settings, key) else "off")
EOF
}
with="$(gfx LD_PRELOAD="$hook" RDFEED_NO_GFX=1 WLOG_LEVEL=OFF)"
if [[ $with == skip ]]; then
echo "skip GFX: FreeRDP 3 is not installed"
else
check "GFX stays off with RDFEED_NO_GFX" "$with" "off"
check "GFX is left alone without it" "$(gfx LD_PRELOAD="$hook" WLOG_LEVEL=OFF)" "on"
fi
exit "$failed"