Implement Desktop Manager Integration for GNOME and KDE (#3500)

Implement full “Display Manager Integration” support for both GNOME and KDE desktop environments. This new feature allows the OneDrive Client for Linux to detect the active desktop session and automatically:

* Register the configured sync_dir as a “special place” or sidebar entry within the file manager (Nautilus on GNOME; Dolphin on KDE).
* Apply a custom “onedrive” folder icon to the synchronisation directory when the installed icon theme supports it.
* Cleanly install and uninstall required resources (icons, bookmarks, file manager integration) via the Makefile’s install and uninstall targets, thereby supporting system-wide installations, packaging workflows, and per-user installs.
* Introduce a new configuration option display_manager_integration (boolean) to enable or disable this integration behaviour at runtime.
* Update documentation and usage guidance to clearly explain what “Display Manager Integration” means, what this client implements (sidebar entry + icon) and what features remain out-of-scope (context menus, overlay badges, tray icons).
* Ensure safe, idempotent integration logic for both GNOME and KDE (bookmark manipulation, icon theme detection, cache refresh) with fallbacks and minimal dependencies.

With this merge, users installing via make install or system packages will benefit from enhanced desktop usability: the OneDrive folder appears visibly and intuitively within their standard file manager sidebar, making access and identification simpler. At the same time, the core sync engine remains focused on reliable file synchronisation, with the desktop integration layer remaining optional and disabled by default unless explicitly enabled via configuration.
This commit is contained in:
abraunegg 2025-11-03 14:26:49 +11:00 committed by GitHub
commit 40e0ca4462
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 527 additions and 1 deletions

View file

@ -49,6 +49,7 @@ BBBA
BBD BBD
bcdefghi bcdefghi
beffdb beffdb
bhref
bindir bindir
bir bir
blargh blargh
@ -181,6 +182,7 @@ GPOs
grecord grecord
groupinstall groupinstall
gshared gshared
gtk
GVariant GVariant
hdrs hdrs
hideonindex hideonindex
@ -215,11 +217,14 @@ itimerspec
journalctl journalctl
jsvar jsvar
kdbx kdbx
kde
keepass keepass
keyboxd keyboxd
keyutils keyutils
kotlin kotlin
krb krb
kservices
kubuntu
lalen lalen
lbl lbl
Lcode Lcode
@ -257,6 +262,7 @@ libnghttp
libnotify libnotify
libphobos libphobos
libpsl libpsl
libqt
libsepol libsepol
libsqlite libsqlite
libssh libssh
@ -512,6 +518,7 @@ wpath
writefln writefln
wrt wrt
wtf wtf
xbel
xca xca
xcbac xcbac
xdeadbeef xdeadbeef

View file

@ -18,6 +18,12 @@ docdir = $(datadir)/doc/$(package)
VPATH = @srcdir@ VPATH = @srcdir@
INSTALL = @INSTALL@ INSTALL = @INSTALL@
# Icon install locations (system-wide hicolor theme)
ICON_THEMEDIR = $(datadir)/icons/hicolor
ICON_PLACES_DIR = $(ICON_THEMEDIR)/scalable/places
ICON_SOURCE_SVG = contrib/images/onedrive.svg
ICON_TARGET_SVG = onedrive.svg
NOTIFICATIONS = @NOTIFICATIONS@ NOTIFICATIONS = @NOTIFICATIONS@
HAVE_SYSTEMD = @HAVE_SYSTEMD@ HAVE_SYSTEMD = @HAVE_SYSTEMD@
systemduserunitdir = @systemduserunitdir@ systemduserunitdir = @systemduserunitdir@
@ -140,6 +146,16 @@ ifeq ($(COMPLETIONS),yes)
mkdir -p $(DESTDIR)$(FISH_COMPLETION_DIR) mkdir -p $(DESTDIR)$(FISH_COMPLETION_DIR)
$(INSTALL) -m 0644 contrib/completions/complete.fish $(DESTDIR)$(FISH_COMPLETION_DIR)/onedrive.fish $(INSTALL) -m 0644 contrib/completions/complete.fish $(DESTDIR)$(FISH_COMPLETION_DIR)/onedrive.fish
endif endif
# --- OneDrive folder icon (hicolor) ---
mkdir -p $(DESTDIR)$(ICON_PLACES_DIR)
$(INSTALL) -m 0644 $(ICON_SOURCE_SVG) $(DESTDIR)$(ICON_PLACES_DIR)/$(ICON_TARGET_SVG)
# Refresh icon cache only when installing to the live system (not during staged DESTDIR installs)
# and only if the theme directory is a proper theme (has index.theme)
if [ -z "$(DESTDIR)" ] && command -v gtk-update-icon-cache >/dev/null 2>&1 \
&& [ -f "$(ICON_THEMEDIR)/index.theme" ]; then \
gtk-update-icon-cache -q "$(ICON_THEMEDIR)"; \
fi
uninstall: uninstall:
rm -f $(DESTDIR)$(bindir)/onedrive rm -f $(DESTDIR)$(bindir)/onedrive
@ -164,3 +180,10 @@ ifeq ($(COMPLETIONS),yes)
rm -f $(DESTDIR)$(BASH_COMPLETION_DIR)/onedrive rm -f $(DESTDIR)$(BASH_COMPLETION_DIR)/onedrive
rm -f $(DESTDIR)$(FISH_COMPLETION_DIR)/onedrive.fish rm -f $(DESTDIR)$(FISH_COMPLETION_DIR)/onedrive.fish
endif endif
# --- OneDrive folder icon (hicolor) ---
rm -f $(DESTDIR)$(ICON_PLACES_DIR)/$(ICON_TARGET_SVG)
# Refresh icon cache if removing from the live system and index.theme exists
if [ -z "$(DESTDIR)" ] && command -v gtk-update-icon-cache >/dev/null 2>&1 \
&& [ -f "$(ICON_THEMEDIR)/index.theme" ]; then \
gtk-update-icon-cache -q "$(ICON_THEMEDIR)"; \
fi

View file

@ -0,0 +1,51 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="35.98 139.2 648.03 430.85">
<defs>
<radialGradient id="radial0" gradientUnits="userSpaceOnUse" cx="0" cy="0" fx="0" fy="0" r="1" gradientTransform="matrix(130.864814,156.804864,-260.089994,217.063603,48.669602,228.766494)">
<stop offset="0" style="stop-color:rgb(28.235294%,58.039216%,99.607843%);stop-opacity:1;"/>
<stop offset="0.695072" style="stop-color:rgb(3.529412%,20.392157%,70.196078%);stop-opacity:1;"/>
</radialGradient>
<radialGradient id="radial1" gradientUnits="userSpaceOnUse" cx="0" cy="0" fx="0" fy="0" r="1" gradientTransform="matrix(-575.289668,663.594003,-491.728488,-426.294267,596.956501,-6.380235)">
<stop offset="0.165327" style="stop-color:rgb(13.72549%,75.294118%,99.607843%);stop-opacity:1;"/>
<stop offset="0.534" style="stop-color:rgb(10.980392%,56.862745%,100%);stop-opacity:1;"/>
</radialGradient>
<radialGradient id="radial2" gradientUnits="userSpaceOnUse" cx="0" cy="0" fx="0" fy="0" r="1" gradientTransform="matrix(-136.753383,-114.806698,262.816935,-313.057562,181.196995,240.395994)">
<stop offset="0" style="stop-color:rgb(100%,100%,100%);stop-opacity:0.4;"/>
<stop offset="0.660528" style="stop-color:rgb(67.843137%,75.294118%,100%);stop-opacity:0;"/>
</radialGradient>
<radialGradient id="radial3" gradientUnits="userSpaceOnUse" cx="0" cy="0" fx="0" fy="0" r="1" gradientTransform="matrix(-153.638428,-130.000063,197.433014,-233.332948,375.353994,451.43549)">
<stop offset="0" style="stop-color:rgb(1.176471%,22.745098%,80%);stop-opacity:1;"/>
<stop offset="1" style="stop-color:rgb(21.176471%,55.686275%,100%);stop-opacity:0;"/>
</radialGradient>
<radialGradient id="radial4" gradientUnits="userSpaceOnUse" cx="0" cy="0" fx="0" fy="0" r="1" gradientTransform="matrix(175.585899,405.198026,-437.434522,189.555055,169.378495,125.589294)">
<stop offset="0.592618" style="stop-color:rgb(20.392157%,39.215686%,89.019608%);stop-opacity:0;"/>
<stop offset="1" style="stop-color:rgb(1.176471%,22.745098%,80%);stop-opacity:0.6;"/>
</radialGradient>
<radialGradient id="radial5" gradientUnits="userSpaceOnUse" cx="0" cy="0" fx="0" fy="0" r="1" gradientTransform="matrix(-459.329491,459.329491,-719.614455,-719.614455,589.876499,39.484649)">
<stop offset="0" style="stop-color:rgb(29.411765%,99.215686%,90.980392%);stop-opacity:0.898039;"/>
<stop offset="0.543937" style="stop-color:rgb(29.411765%,99.215686%,90.980392%);stop-opacity:0;"/>
</radialGradient>
<linearGradient id="linear0" gradientUnits="userSpaceOnUse" x1="29.999701" y1="37.9823" x2="29.999701" y2="18.398199" gradientTransform="matrix(15,0,0,15,0,0)">
<stop offset="0" style="stop-color:rgb(0%,52.54902%,100%);stop-opacity:1;"/>
<stop offset="0.49" style="stop-color:rgb(0%,73.333333%,100%);stop-opacity:1;"/>
</linearGradient>
<radialGradient id="radial6" gradientUnits="userSpaceOnUse" cx="0" cy="0" fx="0" fy="0" r="1" gradientTransform="matrix(273.622108,108.513684,-205.488428,518.148261,296.488495,307.441492)">
<stop offset="0" style="stop-color:rgb(100%,100%,100%);stop-opacity:0.4;"/>
<stop offset="0.785262" style="stop-color:rgb(100%,100%,100%);stop-opacity:0;"/>
</radialGradient>
<radialGradient id="radial7" gradientUnits="userSpaceOnUse" cx="0" cy="0" fx="0" fy="0" r="1" gradientTransform="matrix(-305.683909,263.459223,-264.352324,-306.720147,674.845505,249.378004)">
<stop offset="0" style="stop-color:rgb(29.411765%,99.215686%,90.980392%);stop-opacity:0.898039;"/>
<stop offset="0.584724" style="stop-color:rgb(29.411765%,99.215686%,90.980392%);stop-opacity:0;"/>
</radialGradient>
</defs>
<g id="surface1">
<path style=" stroke:none;fill-rule:nonzero;fill:url(#radial0);" d="M 215.078125 205.089844 C 116.011719 205.09375 41.957031 286.1875 36.382812 376.527344 C 39.835938 395.992188 51.175781 434.429688 68.941406 432.457031 C 91.144531 429.988281 147.066406 432.457031 194.765625 346.105469 C 229.609375 283.027344 301.285156 205.085938 215.078125 205.089844 Z M 215.078125 205.089844 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:url(#radial1);" d="M 192.171875 238.8125 C 158.871094 291.535156 114.042969 367.085938 98.914062 390.859375 C 80.929688 419.121094 33.304688 407.113281 37.25 366.609375 C 36.863281 369.894531 36.5625 373.210938 36.355469 376.546875 C 29.84375 481.933594 113.398438 569.453125 217.375 569.453125 C 331.96875 569.453125 605.269531 426.671875 577.609375 283.609375 C 548.457031 199.519531 466.523438 139.203125 373.664062 139.203125 C 280.808594 139.203125 221.296875 192.699219 192.171875 238.8125 Z M 192.171875 238.8125 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:url(#radial2);" d="M 192.171875 238.8125 C 158.871094 291.535156 114.042969 367.085938 98.914062 390.859375 C 80.929688 419.121094 33.304688 407.113281 37.25 366.609375 C 36.863281 369.894531 36.5625 373.210938 36.355469 376.546875 C 29.84375 481.933594 113.398438 569.453125 217.375 569.453125 C 331.96875 569.453125 605.269531 426.671875 577.609375 283.609375 C 548.457031 199.519531 466.523438 139.203125 373.664062 139.203125 C 280.808594 139.203125 221.296875 192.699219 192.171875 238.8125 Z M 192.171875 238.8125 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:url(#radial3);" d="M 192.171875 238.8125 C 158.871094 291.535156 114.042969 367.085938 98.914062 390.859375 C 80.929688 419.121094 33.304688 407.113281 37.25 366.609375 C 36.863281 369.894531 36.5625 373.210938 36.355469 376.546875 C 29.84375 481.933594 113.398438 569.453125 217.375 569.453125 C 331.96875 569.453125 605.269531 426.671875 577.609375 283.609375 C 548.457031 199.519531 466.523438 139.203125 373.664062 139.203125 C 280.808594 139.203125 221.296875 192.699219 192.171875 238.8125 Z M 192.171875 238.8125 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:url(#radial4);" d="M 192.171875 238.8125 C 158.871094 291.535156 114.042969 367.085938 98.914062 390.859375 C 80.929688 419.121094 33.304688 407.113281 37.25 366.609375 C 36.863281 369.894531 36.5625 373.210938 36.355469 376.546875 C 29.84375 481.933594 113.398438 569.453125 217.375 569.453125 C 331.96875 569.453125 605.269531 426.671875 577.609375 283.609375 C 548.457031 199.519531 466.523438 139.203125 373.664062 139.203125 C 280.808594 139.203125 221.296875 192.699219 192.171875 238.8125 Z M 192.171875 238.8125 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:url(#radial5);" d="M 192.171875 238.8125 C 158.871094 291.535156 114.042969 367.085938 98.914062 390.859375 C 80.929688 419.121094 33.304688 407.113281 37.25 366.609375 C 36.863281 369.894531 36.5625 373.210938 36.355469 376.546875 C 29.84375 481.933594 113.398438 569.453125 217.375 569.453125 C 331.96875 569.453125 605.269531 426.671875 577.609375 283.609375 C 548.457031 199.519531 466.523438 139.203125 373.664062 139.203125 C 280.808594 139.203125 221.296875 192.699219 192.171875 238.8125 Z M 192.171875 238.8125 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:url(#linear0);" d="M 215.699219 569.496094 C 215.699219 569.496094 489.320312 570.035156 535.734375 570.035156 C 619.960938 570.035156 684 501.273438 684 421.03125 C 684 340.789062 618.671875 272.445312 535.734375 272.445312 C 452.792969 272.445312 405.027344 334.492188 369.152344 402.226562 C 327.117188 481.59375 273.488281 568.546875 215.699219 569.496094 Z M 215.699219 569.496094 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:url(#radial6);" d="M 215.699219 569.496094 C 215.699219 569.496094 489.320312 570.035156 535.734375 570.035156 C 619.960938 570.035156 684 501.273438 684 421.03125 C 684 340.789062 618.671875 272.445312 535.734375 272.445312 C 452.792969 272.445312 405.027344 334.492188 369.152344 402.226562 C 327.117188 481.59375 273.488281 568.546875 215.699219 569.496094 Z M 215.699219 569.496094 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:url(#radial7);" d="M 215.699219 569.496094 C 215.699219 569.496094 489.320312 570.035156 535.734375 570.035156 C 619.960938 570.035156 684 501.273438 684 421.03125 C 684 340.789062 618.671875 272.445312 535.734375 272.445312 C 452.792969 272.445312 405.027344 334.492188 369.152344 402.226562 C 327.117188 481.59375 273.488281 568.546875 215.699219 569.496094 Z M 215.699219 569.496094 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8 KiB

View file

@ -22,6 +22,7 @@ Before reading this document, please ensure you are running application version
- [disable_notifications](#disable_notifications) - [disable_notifications](#disable_notifications)
- [disable_permission_set](#disable_permission_set) - [disable_permission_set](#disable_permission_set)
- [disable_upload_validation](#disable_upload_validation) - [disable_upload_validation](#disable_upload_validation)
- [display_manager_integration](#display_manager_integration)
- [display_running_config](#display_running_config) - [display_running_config](#display_running_config)
- [display_transfer_metrics](#display_transfer_metrics) - [display_transfer_metrics](#display_transfer_metrics)
- [dns_timeout](#dns_timeout) - [dns_timeout](#dns_timeout)
@ -343,6 +344,17 @@ _**Config Example:**_ `disable_websocket_support = "false"` or `disable_websocke
_**CLI Option Use:**_ *None - this is a config file option only* _**CLI Option Use:**_ *None - this is a config file option only*
### display_manager_integration
_**Description:**_ Controls whether the client integrates the configured 'sync_dir' with the desktops file manager (e.g. Nautilus for GNOME, Dolphin for KDE), adding it as a “special place” in the sidebar and setting a custom OneDrive folder icon where supported.
_**Value Type:**_ Boolean
_**Default Value:**_ False
_**Config Example:**_ `display_manager_integration = "false"` or `display_manager_integration = "true"`
_**CLI Option Use:**_ *None - this is a config file option only*
### display_running_config ### display_running_config
_**Description:**_ This option will include the running config of the application at application startup. This may be desirable to enable when running in containerised environments so that any application logging that is occurring, will have the application configuration being consumed at startup, written out to any applicable log file. _**Description:**_ This option will include the running config of the application at application startup. This may be desirable to enable when running in containerised environments so that any application logging that is occurring, will have the application configuration being consumed at startup, written out to any applicable log file.

Binary file not shown.

After

Width:  |  Height:  |  Size: 641 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

View file

@ -38,6 +38,7 @@ Before reading this document, please ensure you are running application version
- [Handling a Microsoft OneDrive Account Password Change](#handling-a-microsoft-onedrive-account-password-change) - [Handling a Microsoft OneDrive Account Password Change](#handling-a-microsoft-onedrive-account-password-change)
- [Determining the synchronisation result](#determining-the-synchronisation-result) - [Determining the synchronisation result](#determining-the-synchronisation-result)
- [Resumable Transfers](#resumable-transfers) - [Resumable Transfers](#resumable-transfers)
- [Display Manager Integration](#display-manager-integration)
- [Frequently Asked Configuration Questions](#frequently-asked-configuration-questions) - [Frequently Asked Configuration Questions](#frequently-asked-configuration-questions)
- [How to change the default configuration of the client?](#how-to-change-the-default-configuration-of-the-client) - [How to change the default configuration of the client?](#how-to-change-the-default-configuration-of-the-client)
- [How to change where my data from Microsoft OneDrive is stored?](#how-to-change-where-my-data-from-microsoft-onedrive-is-stored) - [How to change where my data from Microsoft OneDrive is stored?](#how-to-change-where-my-data-from-microsoft-onedrive-is-stored)
@ -1181,6 +1182,69 @@ If `--resync` is used, all resumable data is discarded intentionally.
> [!NOTE] > [!NOTE]
> Resumable transfer support is built-in and requires no special configuration. It is automatically applied during both standalone and monitor operational modes when applicable. > Resumable transfer support is built-in and requires no special configuration. It is automatically applied during both standalone and monitor operational modes when applicable.
### Display Manager Integration
Modern desktop environments such as GNOME and KDE Plasma provide graphical file managers — Nautilus (GNOME Files) and Dolphin, respectively — to help users navigate their local and remote storage.
#### What “Display Manager Integration” means
Display Manager Integration refers to an ability to integrate your configured Microsoft OneDrive synchronisation directory (`sync_dir`) with the desktops file manager environment. Depending on the platform and desktop environment, this may include:
1. **Sidebar registration** — Adding the OneDrive folder as a “special place” within the sidebar of Nautilus (GNOME) or Dolphin (KDE), providing easy access without manual navigation.
2. **Custom folder icon** — Applying a dedicated OneDrive icon to visually distinguish the synchronised directory within the file manager.
3. **Context-menu extensions** — Adding right-click actions such as “Upload to OneDrive” or “Share via OneDrive” directly inside Nautilus or Dolphin.
4. **File overlay badges** — Displaying icons (check-marks, sync arrows, or cloud symbols) to represent file synchronisation state.
5. **System tray or application indicator** — Presenting sync status, pause/resume controls, or notifications via a tray icon.
#### What display manager integration is available in the OneDrive Client for Linux
The OneDrive Client for Linux currently supports the following integration features:
1. **Sidebar registration** — The client automatically registers the OneDrive folder as a “special place” within the sidebar of Nautilus (GNOME) or Dolphin (KDE).
2. **Custom folder icon** — The client applies a OneDrive-specific icon to the synchronisation directory where supported by the installed icon theme.
This behaviour is controlled by the configuration option:
```text
display_manager_integration = "true"
```
When enabled, the client detects the active desktop session and applies the corresponding integration automatically when the client is running in `--monitor` mode only.
> [!NOTE]
> Display Manager Integration remains active only while the OneDrive client or its systemd service is running. If the client stops or the service is stopped, the desktop integration is automatically cleared. It is re-applied the next time the client starts.
#### Fedora Display Manager Integration Example
![fedora_integration](./images/fedora_integration.png)
#### Ubuntu Display Manager Integration Example
![ubuntu_integration](./images/ubuntu_integration.png)
#### Kubuntu Display Manager Integration Example
![kubuntu_integration](./images/kubuntu_integration.png)
#### What about context menu integration?
Context-menu integration is a desktop-specific capability, not part of the core OneDrive Client. It can be achieved through desktop-provided extension mechanisms:
1. **Shell-script bridge** — A simple shell script can be registered as a KDE ServiceMenu or a GNOME Nautilus Script to trigger local actions (for example, creating a symbolic link in `~/OneDrive` to upload a file).
2. **Python + Nautilus API (GNOME)** — Implemented via nautilus-python bindings by registering a subclass of `Nautilus.MenuProvider`.
3. **Qt/KIO Plugins (KDE)** — Implemented using C++ or declarative .desktop ServiceMenu definitions under `/usr/share/kservices5/ServiceMenus/`.
These methods are optional and operate independently of the core OneDrive Client. They can be used by advanced users or system integrators to provide additional right-click functionality.
#### What about file overlay badges?
File overlay badges are typically associated with Microsofts Files-On-Demand feature, which allows selective file downloads and visual state indicators (online-only, available offline, etc.).
Because Files-On-Demand is currently a feature request for this client, overlay badges are not implemented and remain out of scope for now.
#### What about a system tray or application indicator?
While the core OneDrive Client for Linux does not include its own tray icon or GUI dashboard, the community provides complementary tools that plug into it — exposing sync status, pause/resume controls, tray menus, and GUI configuration front-ends. Below are two popular options:
**1. OneDriveGUI** - https://github.com/bpozdena/OneDriveGUI
* A full-featured graphical user interface built for the OneDrive Linux client.
* Key features include: multi-account support, asynchronous real-time monitoring of multiple OneDrive profiles, a setup wizard for profile creation/import, automatic sync on GUI startup, and GUI-based login.
* Includes tray icon support when the desktop environment allows it.
* Intended to simplify one-click configuration of the CLI client, help users visualise current operations (uploads/downloads), and manage advanced features such as SharePoint libraries and multiple profiles.
**2. onedrive_tray** - https://github.com/DanielBorgesOliveira/onedrive_tray
* A lightweight system tray utility written in Qt (using libqt5 or later) that monitors the running OneDrive Linux client and displays status via a tray icon.
* Left-click the tray icon to view sync progress; right-click to access a menu of available actions; middle-click shows the PID of the running client.
* Ideal for users who just want visual status cues (e.g., “sync in progress”, “idle”, “error”) without a full GUI configuration tool.
## Frequently Asked Configuration Questions ## Frequently Asked Configuration Questions

View file

@ -61,6 +61,8 @@ Designed for maximum flexibility and reliability, this powerful and highly confi
* Manages traffic bandwidth use with rate limiting * Manages traffic bandwidth use with rate limiting
.br .br
* Supports sending desktop alerts using libnotify * Supports sending desktop alerts using libnotify
.br
* Provides desktop file-manager integration by registering the OneDrive folder as a sidebar location with a distinctive icon
.SH CONFIGURATION .SH CONFIGURATION
By default, the OneDrive Client for Linux uses a sensible set of built-in defaults to interact with the Microsoft OneDrive service. By default, the OneDrive Client for Linux uses a sensible set of built-in defaults to interact with the Microsoft OneDrive service.

View file

@ -39,6 +39,7 @@ Since forking in early 2018, this client has evolved into a clean re-imagining o
* Enhanced synchronisation speed with multi-threaded file transfers * Enhanced synchronisation speed with multi-threaded file transfers
* Manages traffic bandwidth use with rate limiting * Manages traffic bandwidth use with rate limiting
* Supports sending desktop alerts using libnotify * Supports sending desktop alerts using libnotify
* Provides desktop file-manager integration by registering the OneDrive folder as a sidebar location with a distinctive icon.
## What's missing ## What's missing

View file

@ -19,6 +19,8 @@ import std.format;
import std.ascii; import std.ascii;
import std.datetime; import std.datetime;
import std.exception; import std.exception;
import core.sys.posix.unistd : geteuid, getuid;
import std.process : spawnProcess, wait;
// What other modules that we have created do we need to import? // What other modules that we have created do we need to import?
import log; import log;
@ -444,6 +446,9 @@ class ApplicationConfig {
// Use authentication via OAuth2 Device Authorisation Flow // Use authentication via OAuth2 Device Authorisation Flow
boolValues["use_device_auth"] = false; boolValues["use_device_auth"] = false;
// GUI | Display Manager Integration
boolValues["display_manager_integration"] = false;
// EXPAND USERS HOME DIRECTORY // EXPAND USERS HOME DIRECTORY
// Determine the users home directory. // Determine the users home directory.
// Need to avoid using ~ here as expandTilde() below does not interpret correctly when running under init.d or systemd scripts // Need to avoid using ~ here as expandTilde() below does not interpret correctly when running under init.d or systemd scripts
@ -2821,6 +2826,290 @@ class ApplicationConfig {
recycleBinFilePath = basePath ~ "files/"; recycleBinFilePath = basePath ~ "files/";
recycleBinInfoPath = basePath ~ "info/"; recycleBinInfoPath = basePath ~ "info/";
} }
// Is the client running under a GUI session?
// - GNOME
// - KDE
bool isGuiSessionDetected() {
bool hasDisplay = false;
bool hasRuntime = false;
bool uidMatches = false;
bool homeOK = false;
string xdgType;
try {
xdgType = environment.get("XDG_SESSION_TYPE", "");
} catch (Exception e) {
xdgType = "";
}
try {
hasDisplay = environment.get("WAYLAND_DISPLAY", "").length > 0 || environment.get("DISPLAY", "").length > 0;
} catch (Exception e) {
hasDisplay = false;
}
try {
hasRuntime = environment.get("XDG_RUNTIME_DIR", "").length > 0;
} catch (Exception e) {
hasRuntime = false;
}
try {
uidMatches = (geteuid() == getuid());
} catch (Exception e) {
uidMatches = false;
}
try {
homeOK = environment.get("HOME", "").length > 0;
} catch (Exception e) {
homeOK = false;
}
bool hasGuiElements = hasDisplay || (xdgType == "wayland" || xdgType == "x11");
return hasGuiElements && hasRuntime && uidMatches && homeOK;
}
// Attempt to detect the running display manager
DesktopHints detectDesktop() {
string all = ( environment.get("XDG_CURRENT_DESKTOP","") ~ ":" ~
environment.get("XDG_SESSION_DESKTOP","") ~ ":" ~
environment.get("DESKTOP_SESSION","") ~ ":" ~
environment.get("GDMSESSION","") ~ ":" ~
environment.get("KDE_FULL_SESSION","")).toLower();
DesktopHints hints;
hints.gnome = all.canFind("gnome");
hints.kde = all.canFind("kde") || all.canFind("plasma");
return hints;
}
string fileUriFor(string absPath) {
// Basic, safe URI for local file
return "file://" ~ expandTilde(absPath);
}
void addGnomeBookmark() {
// Configure required variables
string uri = fileUriFor(getValueString("sync_dir"));
string bookmarksPath = buildPath(expandTilde(environment.get("HOME", "")), ".config", "gtk-3.0", "bookmarks");
// Ensure the bookmarks path exists
mkdirRecurse(dirName(bookmarksPath));
// Does the bookmark already exist?
string content = exists(bookmarksPath) ? readText(bookmarksPath) : "";
bool present = false;
foreach (line; content.splitLines()) {
if (line.strip == uri) { present = true; break; }
}
if (present) return;
// Append newline if needed, then our URI
string newline = content.length && !content.endsWith("\n") ? "\n" : "";
string updated = content ~ newline ~ uri ~ "\n";
// Atomic write
string tmp = bookmarksPath ~ ".tmp";
std.file.write(tmp, updated);
rename(tmp, bookmarksPath);
// Log outcome
addLogEntry("GNOME Desktop Integration: Bookmark added successfully", ["info"]);
}
void setOneDriveFolderIcon() {
// Get the sync directory
string syncDir = expandTilde(getValueString("sync_dir"));
// Build gio command
string[] gioCmd = [
"gio",
"set",
syncDir,
"metadata::custom-icon-name",
"onedrive"
];
// Try and set folder icon
try {
auto p = spawnProcess(gioCmd);
int status = p.wait();
if (status == 0) {
addLogEntry("GNOME Desktop Integration: Set folder icon to 'onedrive' for " ~ syncDir, ["info"]);
} else {
addLogEntry("GNOME Desktop Integration: Failed to set folder icon for " ~ syncDir ~ " (gio exit " ~ status.to!string ~ ")", ["info"]);
}
} catch (Exception e) {
addLogEntry("GNOME Desktop Integration: Exception setting folder icon: " ~ e.msg, ["info"]);
}
}
void removeGnomeBookmark() {
// Configure required variables
string uri = fileUriFor(getValueString("sync_dir"));
string bookmarksPath = buildPath(expandTilde(environment.get("HOME", "")), ".config", "gtk-3.0", "bookmarks");
// Does the bookmark path exist?
if (!exists(bookmarksPath)) {
return;
}
// Read existing bookmarks
string content = readText(bookmarksPath);
auto lines = content.splitLines();
bool changed = false;
string[] kept;
kept.reserve(lines.length);
foreach (line; lines) {
// Remove every line that exactly matches the URI (after stripping whitespace)
if (line.strip == uri) {
changed = true;
continue;
}
kept ~= line;
}
if (!changed) {
return;
}
// Rebuild file (ensure trailing newline if non-empty)
string updated = kept.length ? kept.join("\n") ~ "\n" : "";
// Atomic write
const string tmp = bookmarksPath ~ ".tmp";
std.file.write(tmp, updated);
rename(tmp, bookmarksPath);
// Log outcome
addLogEntry("GNOME Desktop Integration: Bookmark removed successfully", ["info"]);
}
void removeOneDriveFolderIcon() {
// Get the sync directory
string syncDir = expandTilde(getValueString("sync_dir"));
// Build gio command
string[] gioCmd = [
"gio",
"set",
syncDir,
"metadata::custom-icon-name",
"folder"
];
// Try and set folder icon
try {
auto p = spawnProcess(gioCmd);
int status = p.wait();
if (status == 0) {
addLogEntry("GNOME Desktop Integration: Reset folder icon to 'default' for " ~ syncDir, ["info"]);
} else {
addLogEntry("GNOME Desktop Integration: Failed to reset folder icon for " ~ syncDir ~ " (gio exit " ~ status.to!string ~ ")", ["info"]);
}
} catch (Exception e) {
addLogEntry("GNOME Desktop Integration: Exception setting folder icon: " ~ e.msg, ["info"]);
}
}
void addKDEPlacesEntry() {
// Configure required variables
string uri = fileUriFor(getValueString("sync_dir"));
string xbelPath = buildPath(expandTilde(environment.get("HOME", "")), ".local", "share", "user-places.xbel");
string content;
// Ensure the xbelPath path exists
mkdirRecurse(dirName(xbelPath));
// Does the xbel file exist?
if (exists(xbelPath)) {
// Path exists - read the file
content = readText(xbelPath);
// Does the 'sync_dir' path exist in the xbel file?
if (content.canFind(`href="` ~ uri ~ `"`)) {
return; // already present
}
} else {
// xbel path does not exist, create minimal XBEL skeleton
content = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<xbel version=\"1.0\">
</xbel>";
}
// Insert xbel bookmark before closing tag
string bookmark = ` <bookmark href="` ~ uri ~ `">
<title>OneDrive</title>
<info>
<metadata owner="http://www.freedesktop.org">
<bookmark:icon name="onedrive"/>
</metadata>
</info>
</bookmark>`;
// Update xbel file with Microsoft OneDrive Bookmark
string updated;
auto idx = content.lastIndexOf("</xbel>");
if (idx >= 0) {
updated = content[0 .. idx] ~ bookmark ~ "\n" ~ content[idx .. $];
} else {
// Fallback: append (still valid for many parsers)
updated = content ~ "\n" ~ bookmark ~ "\n</xbel>\n";
}
string tmp = xbelPath ~ ".tmp";
std.file.write(tmp, updated);
rename(tmp, xbelPath);
// Log outcome
addLogEntry("KDE Desktop Integration: KDE/Plasma place added successfully", ["info"]);
}
void removeKDEPlacesEntry() {
// Compute paths/values
const string uri = fileUriFor(getValueString("sync_dir"));
const string xbelPath = buildPath(expandTilde(environment.get("HOME", "")), ".local", "share", "user-places.xbel");
if (!exists(xbelPath)) {
return;
}
string content = readText(xbelPath);
auto before = content;
// Build a regex that matches:
// <bookmark ... href="URI" ...> ... </bookmark>
// - tolerate attribute order/whitespace
// - accept single or double quotes around URI
// - non-greedy body match
const esc = regexEscape(uri);
auto re = regex(`(?s)<bookmark\b[^>]*\bhref\s*=\s*["']` ~ esc ~ `["'][^>]*>.*?</bookmark\s*>`);
// Remove all matches
content = replaceAll(content, re, "");
// Optional: tidy up multiple blank lines left behind
auto cleanup = regex(`\n{3,}`);
content = replaceAll(content, cleanup, "\n\n");
// If nothing changed, exit quietly
if (content == before) {
return;
}
// Atomic write
string tmp = xbelPath ~ ".tmp";
std.file.write(tmp, content);
rename(tmp, xbelPath);
addLogEntry("KDE Desktop Integration: KDE/Plasma place removed successfully", ["info"]);
}
} }
// Output the full application help when --help is passed in // Output the full application help when --help is passed in

View file

@ -953,6 +953,12 @@ int main(string[] cliArgs) {
if (appConfig.getValueBool("monitor")) { if (appConfig.getValueBool("monitor")) {
// Update the flag given we are running with --monitor // Update the flag given we are running with --monitor
performFileSystemMonitoring = true; performFileSystemMonitoring = true;
// Is Display Manager Integration enabled?
if (appConfig.getValueBool("display_manager_integration")) {
// Attempt to configure the desktop integration whilst the client is running in --monitor mode
attemptFileManagerIntegration();
}
// If 'webhooks' are enabled, this is going to conflict with 'websockets' if the OS cURL library supports websockets // If 'webhooks' are enabled, this is going to conflict with 'websockets' if the OS cURL library supports websockets
if (appConfig.getValueBool("webhook_enabled") && appConfig.curlSupportsWebSockets) { if (appConfig.getValueBool("webhook_enabled") && appConfig.curlSupportsWebSockets) {
@ -1858,6 +1864,14 @@ void performSynchronisedExitProcess(string scopeCaller = null) {
try { try {
// Log who called this function // Log who called this function
if (debugLogging) {addLogEntry("performSynchronisedExitProcess called by: " ~ scopeCaller, ["debug"]);} if (debugLogging) {addLogEntry("performSynchronisedExitProcess called by: " ~ scopeCaller, ["debug"]);}
// Remove Desktop integration
if(performFileSystemMonitoring) {
// Was desktop integration enabled?
if (appConfig.getValueBool("display_manager_integration")) {
// Attempt removal
attemptFileManagerIntegrationRemoval();
}
}
// Shutdown the OneDrive Webhook instance // Shutdown the OneDrive Webhook instance
shutdownOneDriveWebhook(); shutdownOneDriveWebhook();
// Shutdown the OneDrive WebSocket instance // Shutdown the OneDrive WebSocket instance
@ -1994,4 +2008,50 @@ string compilerDetails() {
else enum compiler = "Unknown compiler"; else enum compiler = "Unknown compiler";
string compilerString = compiler ~ " " ~ to!string(__VERSION__); string compilerString = compiler ~ " " ~ to!string(__VERSION__);
return compilerString; return compilerString;
} }
void attemptFileManagerIntegration() {
// Are we running under a Desktop Manager (GNOME or KDE)?
if (appConfig.isGuiSessionDetected()) {
// Generate desktop hints
auto hints = appConfig.detectDesktop();
// GNOME Desktop File Manager integration
if (hints.gnome) {
// Attempt integration
appConfig.addGnomeBookmark();
appConfig.setOneDriveFolderIcon();
return;
}
// KDE Desktop File Manager integration
if (hints.kde) {
// Attempt integration
appConfig.addKDEPlacesEntry();
return;
}
}
}
void attemptFileManagerIntegrationRemoval() {
// Are we running under a Desktop Manager (GNOME or KDE)?
if (appConfig.isGuiSessionDetected()) {
// Generate desktop hints
auto hints = appConfig.detectDesktop();
// GNOME Desktop File Manager integration removal
if (hints.gnome) {
// Attempt integration removal
appConfig.removeGnomeBookmark();
appConfig.removeOneDriveFolderIcon();
return;
}
// KDE Desktop File Manager integration removal
if (hints.kde) {
// Attempt integration removal
appConfig.removeKDEPlacesEntry();
return;
}
}
}

View file

@ -51,6 +51,11 @@ __gshared bool exitHandlerTriggered = false;
// util module variable // util module variable
ulong previousRSS; ulong previousRSS;
struct DesktopHints {
bool gnome;
bool kde;
}
shared static this() { shared static this() {
deviceName = Socket.hostName; deviceName = Socket.hostName;
} }
@ -1943,3 +1948,15 @@ bool isDirEmpty(string dir) {
} }
return true; return true;
} }
// Escape a string for literal use inside a regex
string regexEscape(string s) {
auto b = appender!string();
foreach (c; s) {
// characters with special meaning in regex
immutable specials = "\\.^$|?*+()[]{}";
if (specials.canFind(c)) b.put('\\');
b.put(c);
}
return b.data;
}