initial skills

This commit is contained in:
nrobi144
2025-12-30 15:18:48 +02:00
parent 258c4e0111
commit f654af9d8a
23 changed files with 10898 additions and 0 deletions
@@ -0,0 +1,597 @@
# Desktop Compose APIs Catalog
Complete reference for Compose Multiplatform Desktop-only APIs.
## Window Management
### application
Root entry point for desktop apps.
```kotlin
fun main() = application {
Window(onCloseRequest = ::exitApplication) {
Text("Hello Desktop")
}
}
```
### Window
Creates a window.
```kotlin
Window(
onCloseRequest: () -> Unit,
state: WindowState = rememberWindowState(),
visible: Boolean = true,
title: String = "Untitled",
icon: Painter? = null,
undecorated: Boolean = false,
transparent: Boolean = false,
resizable: Boolean = true,
enabled: Boolean = true,
focusable: Boolean = true,
alwaysOnTop: Boolean = false,
onPreviewKeyEvent: ((KeyEvent) -> Boolean) = { false },
onKeyEvent: ((KeyEvent) -> Boolean) = { false },
content: @Composable FrameWindowScope.() -> Unit
)
```
**Example:**
```kotlin
val windowState = rememberWindowState(
width = 1200.dp,
height = 800.dp,
position = WindowPosition.Aligned(Alignment.Center)
)
Window(
onCloseRequest = ::exitApplication,
state = windowState,
title = "My App",
resizable = true
) {
// Content
}
```
### rememberWindowState
Manages window size and position.
```kotlin
@Composable
fun rememberWindowState(
placement: WindowPlacement = WindowPlacement.Floating,
isMinimized: Boolean = false,
position: WindowPosition = WindowPosition.PlatformDefault,
width: Dp = Dp.Unspecified,
height: Dp = Dp.Unspecified
): WindowState
```
**WindowPlacement:**
- `Floating` - Normal window
- `Maximized` - Fullscreen
- `Fullscreen` - Fullscreen without decorations
**WindowPosition:**
- `PlatformDefault` - OS decides
- `Aligned(alignment)` - Center, TopStart, etc.
- `Absolute(x, y)` - Fixed position in pixels
### DialogWindow
Modal dialog.
```kotlin
DialogWindow(
onCloseRequest: () -> Unit,
state: DialogState = rememberDialogState(),
visible: Boolean = true,
title: String = "Dialog",
icon: Painter? = null,
undecorated: Boolean = false,
transparent: Boolean = false,
resizable: Boolean = true,
enabled: Boolean = true,
focusable: Boolean = true,
content: @Composable DialogWindowScope.() -> Unit
)
```
**Example:**
```kotlin
var showDialog by remember { mutableStateOf(false) }
if (showDialog) {
DialogWindow(
onCloseRequest = { showDialog = false },
title = "Confirm"
) {
Column(Modifier.padding(16.dp)) {
Text("Are you sure?")
Row {
Button(onClick = { showDialog = false }) {
Text("Cancel")
}
Button(onClick = { /* confirm */ }) {
Text("OK")
}
}
}
}
}
```
---
## MenuBar
### MenuBar
Native menu bar for windows.
```kotlin
@Composable
fun FrameWindowScope.MenuBar(
content: @Composable MenuBarScope.() -> Unit
)
```
**Example:**
```kotlin
Window(onCloseRequest = ::exitApplication) {
MenuBar {
Menu("File") {
Item("New", onClick = { /* ... */ })
Item("Open", onClick = { /* ... */ })
Separator()
Item("Quit", onClick = ::exitApplication)
}
Menu("Edit") {
Item("Copy", onClick = { /* ... */ })
Item("Paste", onClick = { /* ... */ })
}
}
}
```
### Menu
Top-level menu.
```kotlin
@Composable
fun MenuBarScope.Menu(
text: String,
mnemonic: Char? = null,
enabled: Boolean = true,
content: @Composable MenuScope.() -> Unit
)
```
### Item
Menu item.
```kotlin
@Composable
fun MenuScope.Item(
text: String,
onClick: () -> Unit,
shortcut: KeyShortcut? = null,
mnemonic: Char? = null,
enabled: Boolean = true,
icon: Painter? = null
)
```
**With keyboard shortcut:**
```kotlin
Item(
text = "Save",
onClick = { save() },
shortcut = KeyShortcut(Key.S, ctrl = true),
icon = painterResource("save.png")
)
```
### Separator
Menu separator line.
```kotlin
@Composable
fun MenuScope.Separator()
```
### CheckboxItem
Toggleable menu item.
```kotlin
@Composable
fun MenuScope.CheckboxItem(
text: String,
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
shortcut: KeyShortcut? = null,
mnemonic: Char? = null,
enabled: Boolean = true
)
```
**Example:**
```kotlin
var darkMode by remember { mutableStateOf(false) }
Menu("View") {
CheckboxItem(
text = "Dark Mode",
checked = darkMode,
onCheckedChange = { darkMode = it },
shortcut = KeyShortcut(Key.D, ctrl = true)
)
}
```
### RadioButtonItem
Radio button menu item.
```kotlin
@Composable
fun MenuScope.RadioButtonItem(
text: String,
selected: Boolean,
onClick: () -> Unit,
shortcut: KeyShortcut? = null,
mnemonic: Char? = null,
enabled: Boolean = true
)
```
---
## System Tray
### Tray
System tray icon with menu.
```kotlin
@Composable
fun ApplicationScope.Tray(
icon: Painter,
state: TrayState = rememberTrayState(),
tooltip: String? = null,
onAction: () -> Unit = {},
menu: @Composable MenuScope.() -> Unit = {}
)
```
**Example:**
```kotlin
application {
var isVisible by remember { mutableStateOf(true) }
Tray(
icon = painterResource("tray-icon.png"),
tooltip = "My App",
onAction = { isVisible = true },
menu = {
Item("Show Window", onClick = { isVisible = true })
Separator()
Item("Quit", onClick = ::exitApplication)
}
)
if (isVisible) {
Window(
onCloseRequest = { isVisible = false },
title = "App"
) {
// Content
}
}
}
```
### rememberTrayState
Manages tray state.
```kotlin
@Composable
fun rememberTrayState(): TrayState
```
---
## Notifications
### Notification (via Tray)
Show desktop notifications through tray.
```kotlin
val trayState = rememberTrayState()
Tray(
icon = painterResource("icon.png"),
state = trayState
)
// Send notification
LaunchedEffect(Unit) {
trayState.sendNotification(
Notification(
title = "Message",
message = "You have a new message",
type = Notification.Type.Info
)
)
}
```
**Notification types:**
- `Info` - Information
- `Warning` - Warning
- `Error` - Error
---
## Keyboard
### KeyShortcut
Keyboard shortcut definition.
```kotlin
data class KeyShortcut(
val key: Key,
val ctrl: Boolean = false,
val meta: Boolean = false,
val alt: Boolean = false,
val shift: Boolean = false
)
```
**Examples:**
```kotlin
// Ctrl+S (Windows/Linux)
KeyShortcut(Key.S, ctrl = true)
// Cmd+S (macOS)
KeyShortcut(Key.S, meta = true)
// Ctrl+Shift+N
KeyShortcut(Key.N, ctrl = true, shift = true)
// Alt+F4
KeyShortcut(Key.F4, alt = true)
```
### onPreviewKeyEvent / onKeyEvent
Window-level keyboard handlers.
```kotlin
Window(
onCloseRequest = ::exitApplication,
onPreviewKeyEvent = { event ->
if (event.key == Key.Escape && event.type == KeyEventType.KeyDown) {
// Handle Escape
true // Consume event
} else {
false // Propagate
}
}
) {
// Content
}
```
---
## Mouse
### PointerMoveFilter (Deprecated, use Modifier.pointerInput)
```kotlin
Box(
modifier = Modifier
.pointerInput(Unit) {
awaitPointerEventScope {
while (true) {
val event = awaitPointerEvent()
// Handle mouse events
}
}
}
)
```
### Mouse cursor
```kotlin
Box(
modifier = Modifier.pointerHoverIcon(
icon = PointerIcon(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR))
)
) {
Text("Hover me")
}
```
**Cursor types:**
- `DEFAULT_CURSOR`
- `HAND_CURSOR`
- `TEXT_CURSOR`
- `CROSSHAIR_CURSOR`
- `WAIT_CURSOR`
- `MOVE_CURSOR`
- `E_RESIZE_CURSOR`, `W_RESIZE_CURSOR`, etc.
---
## Drag & Drop (Experimental)
### onExternalDrag
Handle drag-and-drop from external sources.
```kotlin
Box(
modifier = Modifier
.size(200.dp)
.background(Color.LightGray)
.onExternalDrag(
onDragStart = { externalDragValue ->
println("Drag started")
},
onDrag = { externalDragValue ->
println("Dragging: ${externalDragValue.dragData}")
},
onDragExit = {
println("Drag exited")
},
onDrop = { externalDragValue ->
val dragData = externalDragValue.dragData
when (dragData) {
is DragData.FilesList -> {
println("Files dropped: ${dragData.readFiles()}")
}
is DragData.Text -> {
println("Text dropped: ${dragData.readText()}")
}
else -> {}
}
}
)
) {
Text("Drop files here", Modifier.align(Alignment.Center))
}
```
---
## Resources
### painterResource
Load images from resources.
```kotlin
val icon = painterResource("icon.png")
Icon(
painter = icon,
contentDescription = "App icon"
)
```
**Resource location:** `src/jvmMain/resources/`
---
## Platform Integration
### Desktop.getDesktop() (AWT)
Access system desktop features (not Compose API, but commonly used).
```kotlin
import java.awt.Desktop
import java.net.URI
// Open URL in browser
if (Desktop.isDesktopSupported()) {
Desktop.getDesktop().browse(URI("https://example.com"))
}
// Open file with default app
Desktop.getDesktop().open(File("/path/to/file.pdf"))
// Open email client
Desktop.getDesktop().mail(URI("mailto:user@example.com"))
```
### FileDialog (AWT)
File picker dialogs.
```kotlin
import java.awt.FileDialog
import java.awt.Frame
// Open file
val fileDialog = FileDialog(Frame(), "Select file", FileDialog.LOAD)
fileDialog.isVisible = true
val selectedFile = fileDialog.file
val directory = fileDialog.directory
// Save file
val saveDialog = FileDialog(Frame(), "Save file", FileDialog.SAVE)
saveDialog.file = "document.txt"
saveDialog.isVisible = true
```
---
## SwingPanel (Interop)
Embed Swing components in Compose.
```kotlin
import androidx.compose.ui.awt.SwingPanel
import javax.swing.JButton
SwingPanel(
factory = {
JButton("Swing Button").apply {
addActionListener {
println("Swing button clicked")
}
}
},
modifier = Modifier.size(200.dp, 50.dp)
)
```
---
## ComposePanel (Reverse Interop)
Embed Compose in Swing.
```kotlin
import androidx.compose.ui.awt.ComposePanel
import javax.swing.JFrame
val frame = JFrame("Swing Frame")
val composePanel = ComposePanel()
composePanel.setContent {
Text("Compose in Swing")
}
frame.contentPane.add(composePanel)
frame.setSize(400, 300)
frame.isVisible = true
```
---
## Version Requirements
- **Kotlin:** 2.0+
- **Compose Multiplatform:** 1.7.0+
- **JVM Target:** 11+ (recommend 21)
**See also:**
- [Official Desktop API docs](https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-desktop-components.html)
- [Compose Multiplatform repo](https://github.com/JetBrains/compose-multiplatform)
@@ -0,0 +1,464 @@
# Desktop Navigation Patterns
Comparison of mobile vs desktop navigation patterns in AmethystMultiplatform.
## Core Difference
| Platform | Pattern | Location | Rationale |
|----------|---------|----------|-----------|
| **Android** | Bottom Navigation Bar | Horizontal, bottom | Thumb reach on mobile |
| **Desktop** | Navigation Rail | Vertical, left sidebar | Horizontal screen space |
---
## Desktop: NavigationRail
### Current Implementation
**File:** `desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt:191-264`
```kotlin
@Composable
fun MainContent(
currentScreen: AppScreen,
onScreenChange: (AppScreen) -> Unit,
// ...
) {
Row(Modifier.fillMaxSize()) {
// LEFT: Vertical Sidebar (NavigationRail)
NavigationRail(
modifier = Modifier.width(80.dp).fillMaxHeight(),
containerColor = MaterialTheme.colorScheme.surfaceVariant
) {
Spacer(Modifier.height(16.dp))
// Top navigation items
NavigationRailItem(
icon = { Icon(Icons.Default.Home, "Feed") },
label = { Text("Feed") },
selected = currentScreen == AppScreen.Feed,
onClick = { onScreenChange(AppScreen.Feed) }
)
NavigationRailItem(
icon = { Icon(Icons.Default.Search, "Search") },
label = { Text("Search") },
selected = currentScreen == AppScreen.Search,
onClick = { onScreenChange(AppScreen.Search) }
)
NavigationRailItem(
icon = { Icon(Icons.Default.Email, "Messages") },
label = { Text("DMs") },
selected = currentScreen == AppScreen.Messages,
onClick = { onScreenChange(AppScreen.Messages) }
)
NavigationRailItem(
icon = { Icon(Icons.Default.Notifications, "Notifications") },
label = { Text("Alerts") },
selected = currentScreen == AppScreen.Notifications,
onClick = { onScreenChange(AppScreen.Notifications) }
)
NavigationRailItem(
icon = { Icon(Icons.Default.Person, "Profile") },
label = { Text("Profile") },
selected = currentScreen == AppScreen.Profile,
onClick = { onScreenChange(AppScreen.Profile) }
)
// Push Settings to bottom
Spacer(Modifier.weight(1f))
HorizontalDivider(Modifier.padding(horizontal = 16.dp))
NavigationRailItem(
icon = { Icon(Icons.Default.Settings, "Settings") },
label = { Text("Settings") },
selected = currentScreen == AppScreen.Settings,
onClick = { onScreenChange(AppScreen.Settings) }
)
Spacer(Modifier.height(16.dp))
}
VerticalDivider()
// RIGHT: Main Content Area
Box(modifier = Modifier.weight(1f).fillMaxHeight().padding(24.dp)) {
when (currentScreen) {
AppScreen.Feed -> FeedScreen(relayManager)
AppScreen.Search -> SearchPlaceholder()
AppScreen.Messages -> MessagesPlaceholder()
AppScreen.Notifications -> NotificationsPlaceholder()
AppScreen.Profile -> ProfileScreen(account, accountManager)
AppScreen.Settings -> RelaySettingsScreen(relayManager)
}
}
}
}
```
### Layout Structure
```
┌────────────────────────────────────────┐
│ [Menu Bar: File, Edit, View, Help] │ ← MenuBar (OS-native)
├──────┬─────────────────────────────────┤
│ │ │
│ [🏠] │ │
│ Feed │ │
│ │ │
│ [🔍] │ Main Content Area │
│Search│ (Feed, Messages, etc.) │
│ │ │
│ [✉️] │ │
│ DMs │ │
│ │ │
│ [🔔] │ │
│Alerts│ │
│ │ │
│ [👤] │ │
│Profile │
│ │ │
│ ─ │ │
│ [⚙️] │ │
│Settings │
│ │ │
└──────┴─────────────────────────────────┘
80dp Remaining width (weight=1f)
```
### Key Features
1. **Always visible:** All nav items visible at once
2. **Icon + Label:** Both shown (not just icons)
3. **Vertical list:** Natural reading order
4. **Settings at bottom:** Separated by divider + Spacer.weight(1f)
5. **80dp width:** Standard NavigationRail width
---
## Android: BottomNavigationBar (Future)
### Expected Implementation
**Location:** `amethyst/src/androidMain/kotlin/...` (not yet implemented)
```kotlin
@Composable
fun MainScreen(
currentScreen: AppScreen,
onScreenChange: (AppScreen) -> Unit
) {
Scaffold(
bottomBar = {
NavigationBar {
NavigationBarItem(
icon = { Icon(Icons.Default.Home, "Feed") },
label = { Text("Feed") },
selected = currentScreen == AppScreen.Feed,
onClick = { onScreenChange(AppScreen.Feed) }
)
NavigationBarItem(
icon = { Icon(Icons.Default.Search, "Search") },
label = { Text("Search") },
selected = currentScreen == AppScreen.Search,
onClick = { onScreenChange(AppScreen.Search) }
)
NavigationBarItem(
icon = { Icon(Icons.Default.Email, "Messages") },
label = { Text("Messages") },
selected = currentScreen == AppScreen.Messages,
onClick = { onScreenChange(AppScreen.Messages) }
)
NavigationBarItem(
icon = { Icon(Icons.Default.Person, "Profile") },
label = { Text("Profile") },
selected = currentScreen == AppScreen.Profile,
onClick = { onScreenChange(AppScreen.Profile) }
)
}
}
) { paddingValues ->
Box(Modifier.padding(paddingValues)) {
when (currentScreen) {
AppScreen.Feed -> FeedScreen()
AppScreen.Search -> SearchScreen()
AppScreen.Messages -> MessagesScreen()
AppScreen.Profile -> ProfileScreen()
// Settings accessed via Profile or overflow menu
}
}
}
}
```
### Layout Structure
```
┌─────────────────────────────────────┐
│ │
│ │
│ Main Content Area │
│ (Feed, Messages, etc.) │
│ │
│ │
│ │
├─────────────────────────────────────┤
│ [🏠] [🔍] [✉️] [👤] │ ← NavigationBar
│ Feed Search DMs Profile │
└─────────────────────────────────────┘
```
### Key Differences from Desktop
1. **Bottom placement:** Thumb reach
2. **Horizontal layout:** Limited vertical space
3. **Fewer items:** 3-5 primary destinations
4. **Label optional:** Can hide on small screens
5. **Settings hidden:** In profile or overflow
---
## Shared Navigation State
Both platforms use the same `AppScreen` enum from `commons`.
**File:** `commons/src/commonMain/kotlin/.../navigation/AppScreen.kt` (expected)
```kotlin
// Shared navigation destinations
enum class AppScreen {
Feed,
Search,
Messages,
Notifications,
Profile,
Settings
}
```
**State management (shared):**
```kotlin
// commons/src/jvmAndroid/kotlin/.../navigation/NavigationViewModel.kt
class NavigationViewModel : ViewModel() {
private val _currentScreen = MutableStateFlow(AppScreen.Feed)
val currentScreen: StateFlow<AppScreen> = _currentScreen.asStateFlow()
fun navigateTo(screen: AppScreen) {
_currentScreen.value = screen
}
}
```
---
## Multi-Pane Desktop Layout (Advanced)
Desktop can utilize horizontal space for multi-pane layouts.
### Two-Pane Layout
```kotlin
Row(Modifier.fillMaxSize()) {
// Left: NavigationRail (fixed 80dp)
NavigationRail { /* ... */ }
VerticalDivider()
// Center: Main content (60% width)
Box(Modifier.weight(0.6f)) {
FeedScreen()
}
// Right: Detail pane (40% width, conditional)
if (selectedNote != null) {
VerticalDivider()
Box(Modifier.weight(0.4f)) {
NoteDetailPane(selectedNote)
}
}
}
```
### Layout:
```
┌──────┬───────────────────┬─────────────┐
│ │ │ │
│ Nav │ Feed List │ Detail │
│ Rail │ (60%) │ Pane │
│ │ │ (40%) │
│ │ │ │
└──────┴───────────────────┴─────────────┘
80dp weight(0.6f) weight(0.4f)
```
**Use cases:**
- Email: List + message detail
- Notes: List + editor
- Settings: Categories + options
---
## Keyboard Navigation
Desktop should support keyboard navigation.
### Tab Navigation
```kotlin
NavigationRail(
modifier = Modifier.focusable()
) {
NavigationRailItem(
icon = { Icon(Icons.Default.Home, "Feed") },
label = { Text("Feed") },
selected = currentScreen == AppScreen.Feed,
onClick = { onScreenChange(AppScreen.Feed) },
modifier = Modifier.focusable()
)
// More items...
}
```
### Keyboard Shortcuts
```kotlin
Window(
onPreviewKeyEvent = { event ->
when {
event.key == Key.One && event.isCtrlPressed ->
onScreenChange(AppScreen.Feed).also { true }
event.key == Key.Two && event.isCtrlPressed ->
onScreenChange(AppScreen.Search).also { true }
event.key == Key.Three && event.isCtrlPressed ->
onScreenChange(AppScreen.Messages).also { true }
else -> false
}
}
) {
// Content
}
```
**Standard:**
- Ctrl+1: First nav item (Feed)
- Ctrl+2: Second nav item (Search)
- Ctrl+3: Third nav item (Messages)
- Ctrl+Comma: Settings
---
## Navigation Transitions
### Desktop (Instant)
No fancy animations. Instant switch.
```kotlin
Box {
when (currentScreen) {
AppScreen.Feed -> FeedScreen()
AppScreen.Search -> SearchScreen()
}
}
```
### Android (Animated, Future)
Can use Navigation Compose for transitions.
```kotlin
NavHost(navController, startDestination = "feed") {
composable("feed") { FeedScreen() }
composable("search") { SearchScreen() }
}
```
---
## Best Practices
### Desktop NavigationRail
**DO:**
- Keep width 72-80dp
- Show both icon and label
- Use Spacer.weight(1f) for bottom items
- Separate sections with HorizontalDivider
- Limit to 5-7 primary items
**DON'T:**
- Use bottom navigation on desktop
- Hide labels (plenty of space)
- Make it collapsible (not standard)
- Use hamburger menu (not desktop pattern)
### Android NavigationBar
**DO:**
- Limit to 3-5 items
- Use bottom placement
- Consider label visibility on small screens
- Use standard icons
**DON'T:**
- Put more than 5 items
- Use top placement (deprecated)
- Put critical actions only in nav bar
---
## Migration Strategy
When adding Android support:
1. **Extract shared state:** Move `AppScreen` to `commons/commonMain`
2. **Platform layouts:** Keep `NavigationRail` in `desktopApp/jvmMain`, `NavigationBar` in `amethyst/androidMain`
3. **Shared screens:** Composables in `commons/commonMain` (FeedScreen content)
4. **Platform chrome:** Navigation containers in platform modules
**Example:**
```kotlin
// commons/commonMain - Shared screen content
@Composable
fun FeedContent(notes: List<Note>) {
LazyColumn {
items(notes) { note ->
NoteCard(note)
}
}
}
// desktopApp/jvmMain - Desktop wrapper
@Composable
fun FeedScreen() {
Column {
FeedHeader() // Desktop-specific header
FeedContent(notes) // Shared content
}
}
// amethyst/androidMain - Android wrapper
@Composable
fun FeedScreen() {
Scaffold(
topBar = { TopAppBar { Text("Feed") } }
) {
FeedContent(notes) // Same shared content
}
}
```
---
## References
- **Current Desktop:** Main.kt:191-264
- **Material3 NavigationRail:** [Material Design Docs](https://m3.material.io/components/navigation-rail)
- **Material3 NavigationBar:** [Material Design Docs](https://m3.material.io/components/navigation-bar)
@@ -0,0 +1,400 @@
# Keyboard Shortcuts Reference
Standard keyboard shortcuts for desktop applications across macOS, Windows, and Linux.
## Primary Modifier Keys
| Platform | Primary | Secondary | Tertiary |
|----------|---------|-----------|----------|
| **macOS** | Cmd (⌘) / `meta` | Option (⌥) / `alt` | Ctrl (⌃) / `ctrl` |
| **Windows** | Ctrl / `ctrl` | Alt / `alt` | Win / `meta` |
| **Linux** | Ctrl / `ctrl` | Alt / `alt` | Super / `meta` |
**In Compose Desktop:**
```kotlin
// macOS
KeyShortcut(Key.N, meta = true) // Cmd+N
// Windows/Linux
KeyShortcut(Key.N, ctrl = true) // Ctrl+N
```
---
## File Operations
| Action | macOS | Windows | Linux | Notes |
|--------|-------|---------|-------|-------|
| **New** | Cmd+N | Ctrl+N | Ctrl+N | Create new |
| **Open** | Cmd+O | Ctrl+O | Ctrl+O | Open file |
| **Save** | Cmd+S | Ctrl+S | Ctrl+S | Save current |
| **Save As** | Cmd+Shift+S | Ctrl+Shift+S | Ctrl+Shift+S | Save with new name |
| **Close** | Cmd+W | Ctrl+W | Ctrl+W | Close window/tab |
| **Quit** | Cmd+Q | Ctrl+Q | Ctrl+Q | Exit app |
| **Print** | Cmd+P | Ctrl+P | Ctrl+P | Print |
**Compose Implementation:**
```kotlin
val isMacOS = System.getProperty("os.name").lowercase().contains("mac")
MenuBar {
Menu("File") {
Item(
"New Note",
shortcut = if (isMacOS) {
KeyShortcut(Key.N, meta = true)
} else {
KeyShortcut(Key.N, ctrl = true)
},
onClick = { createNewNote() }
)
Item(
"Save",
shortcut = if (isMacOS) {
KeyShortcut(Key.S, meta = true)
} else {
KeyShortcut(Key.S, ctrl = true)
},
onClick = { save() }
)
Separator()
Item(
"Quit",
shortcut = if (isMacOS) {
KeyShortcut(Key.Q, meta = true)
} else {
KeyShortcut(Key.Q, ctrl = true)
},
onClick = ::exitApplication
)
}
}
```
---
## Edit Operations
| Action | macOS | Windows | Linux | Notes |
|--------|-------|---------|-------|-------|
| **Undo** | Cmd+Z | Ctrl+Z | Ctrl+Z | Universal |
| **Redo** | Cmd+Shift+Z | Ctrl+Y | Ctrl+Y | Windows/Linux use Y |
| **Cut** | Cmd+X | Ctrl+X | Ctrl+X | Universal |
| **Copy** | Cmd+C | Ctrl+C | Ctrl+C | Universal |
| **Paste** | Cmd+V | Ctrl+V | Ctrl+V | Universal |
| **Select All** | Cmd+A | Ctrl+A | Ctrl+A | Universal |
| **Find** | Cmd+F | Ctrl+F | Ctrl+F | Search |
| **Find Next** | Cmd+G | F3 | F3 | Next result |
| **Replace** | Cmd+Option+F | Ctrl+H | Ctrl+H | Find & replace |
**Note:** Undo/Redo typically handled by text fields automatically.
---
## Navigation
| Action | macOS | Windows | Linux | Notes |
|--------|-------|---------|-------|-------|
| **Tab 1** | Cmd+1 | Ctrl+1 | Ctrl+1 | First tab/view |
| **Tab 2** | Cmd+2 | Ctrl+2 | Ctrl+2 | Second tab/view |
| **Tab 3** | Cmd+3 | Ctrl+3 | Ctrl+3 | Third tab/view |
| **Next Tab** | Cmd+Option+→ | Ctrl+Tab | Ctrl+Tab | Cycle forward |
| **Prev Tab** | Cmd+Option+← | Ctrl+Shift+Tab | Ctrl+Shift+Tab | Cycle back |
| **Go Back** | Cmd+[ | Alt+← | Alt+← | Browser-style |
| **Go Forward** | Cmd+] | Alt+→ | Alt+→ | Browser-style |
**Compose Implementation:**
```kotlin
Window(
onPreviewKeyEvent = { event ->
if (event.type == KeyEventType.KeyDown) {
when {
event.key == Key.One && event.isPrimaryPressed() -> {
navigateTo(AppScreen.Feed)
true
}
event.key == Key.Two && event.isPrimaryPressed() -> {
navigateTo(AppScreen.Search)
true
}
event.key == Key.Three && event.isPrimaryPressed() -> {
navigateTo(AppScreen.Messages)
true
}
else -> false
}
} else false
}
) {
// Content
}
// Helper extension
fun KeyEvent.isPrimaryPressed() = if (isMacOS) isMetaPressed else isCtrlPressed
```
---
## Window Management
| Action | macOS | Windows | Linux | Notes |
|--------|-------|---------|-------|-------|
| **New Window** | Cmd+N | Ctrl+N | Ctrl+N | New instance |
| **Close Window** | Cmd+W | Alt+F4 | Alt+F4 | Close current |
| **Minimize** | Cmd+M | Win+Down | Super+Down | Minimize to dock/taskbar |
| **Maximize** | Cmd+Ctrl+F | Win+Up | Super+Up | Fullscreen/maximize |
| **Hide App** | Cmd+H | - | - | macOS only |
| **Switch Window** | Cmd+` | Alt+Tab | Alt+Tab | Between app windows |
**Note:** Window management often handled by OS, not app shortcuts.
---
## App-Specific (Amethyst)
### Nostr Actions
| Action | macOS | Windows | Linux | Description |
|--------|-------|---------|-------|-------------|
| **New Note** | Cmd+N | Ctrl+N | Ctrl+N | Compose new post |
| **Refresh Feed** | Cmd+R | Ctrl+R | Ctrl+R | Reload timeline |
| **Search** | Cmd+K | Ctrl+K | Ctrl+K | Quick search |
| **DMs** | Cmd+Shift+M | Ctrl+Shift+M | Ctrl+Shift+M | Open messages |
| **Settings** | Cmd+, | Ctrl+, | Ctrl+, | Open preferences |
| **Notifications** | Cmd+Shift+N | Ctrl+Shift+N | Ctrl+Shift+N | View alerts |
**Implementation:**
```kotlin
MenuBar {
Menu("File") {
Item(
"New Note",
shortcut = DesktopShortcuts.primary(Key.N),
onClick = { showComposeDialog() }
)
Item(
"Settings",
shortcut = DesktopShortcuts.primary(Key.Comma),
onClick = { navigateTo(AppScreen.Settings) }
)
}
Menu("View") {
Item(
"Refresh Feed",
shortcut = DesktopShortcuts.primary(Key.R),
onClick = { refreshFeed() }
)
Item(
"Search",
shortcut = DesktopShortcuts.primary(Key.K),
onClick = { focusSearch() }
)
}
}
```
---
## Accessibility
| Action | macOS | Windows | Linux | Description |
|--------|-------|---------|-------|-------------|
| **Zoom In** | Cmd++ | Ctrl++ | Ctrl++ | Increase size |
| **Zoom Out** | Cmd+- | Ctrl+- | Ctrl+- | Decrease size |
| **Reset Zoom** | Cmd+0 | Ctrl+0 | Ctrl+0 | Default size |
| **Help** | Cmd+? | F1 | F1 | Show help |
---
## Best Practices
### 1. OS-Aware Helper
Create a utility for OS detection:
```kotlin
// commons/src/jvmMain/kotlin/utils/PlatformShortcuts.kt
object DesktopShortcuts {
private val isMacOS = System.getProperty("os.name")
.lowercase()
.contains("mac")
fun primary(key: Key) = if (isMacOS) {
KeyShortcut(key, meta = true)
} else {
KeyShortcut(key, ctrl = true)
}
fun primaryShift(key: Key) = if (isMacOS) {
KeyShortcut(key, meta = true, shift = true)
} else {
KeyShortcut(key, ctrl = true, shift = true)
}
fun primaryAlt(key: Key) = if (isMacOS) {
KeyShortcut(key, meta = true, alt = true)
} else {
KeyShortcut(key, ctrl = true, alt = true)
}
val modifierName = if (isMacOS) "Cmd" else "Ctrl"
val secondaryName = if (isMacOS) "Option" else "Alt"
}
```
**Usage:**
```kotlin
Item(
"Save",
shortcut = DesktopShortcuts.primary(Key.S),
onClick = { save() }
)
```
### 2. Show Shortcuts in Tooltips
```kotlin
IconButton(
onClick = { refresh() },
modifier = Modifier.tooltipArea {
Text("Refresh (${DesktopShortcuts.modifierName}+R)")
}
) {
Icon(Icons.Default.Refresh, "Refresh")
}
```
### 3. Shortcuts Menu
Provide a "Keyboard Shortcuts" help menu:
```kotlin
Menu("Help") {
Item("Keyboard Shortcuts", onClick = { showShortcutsDialog() })
}
// Dialog content
@Composable
fun ShortcutsDialog() {
Dialog(onDismissRequest = { /* close */ }) {
Surface {
Column(Modifier.padding(16.dp)) {
Text("Keyboard Shortcuts", style = MaterialTheme.typography.headlineMedium)
Spacer(Modifier.height(16.dp))
ShortcutRow("New Note", "${DesktopShortcuts.modifierName}+N")
ShortcutRow("Save", "${DesktopShortcuts.modifierName}+S")
ShortcutRow("Search", "${DesktopShortcuts.modifierName}+K")
ShortcutRow("Settings", "${DesktopShortcuts.modifierName}+,")
// ...
}
}
}
}
@Composable
fun ShortcutRow(action: String, shortcut: String) {
Row(
Modifier.fillMaxWidth().padding(vertical = 4.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(action, style = MaterialTheme.typography.bodyMedium)
Text(
shortcut,
style = MaterialTheme.typography.bodySmall,
fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
```
### 4. Avoid Conflicts
**Check for OS-level shortcuts:**
| macOS Reserved | Description |
|----------------|-------------|
| Cmd+Tab | Switch apps |
| Cmd+Space | Spotlight |
| Cmd+H | Hide window |
| Cmd+M | Minimize |
| Cmd+Q | Quit |
| Cmd+W | Close window |
**Windows Reserved:**
| Windows Reserved | Description |
|-----------------|-------------|
| Win+D | Show desktop |
| Win+E | File Explorer |
| Win+L | Lock screen |
| Alt+Tab | Switch apps |
| Alt+F4 | Close window |
**Don't override these unless critical.**
---
## Testing Shortcuts
```kotlin
// Test OS detection
@Test
fun testOsDetection() {
val osName = System.getProperty("os.name")
println("OS: $osName")
val isMacOS = osName.lowercase().contains("mac")
println("Is macOS: $isMacOS")
val shortcut = if (isMacOS) {
KeyShortcut(Key.N, meta = true)
} else {
KeyShortcut(Key.N, ctrl = true)
}
println("Primary modifier for New: $shortcut")
}
```
---
## Current Issues in Amethyst
**Main.kt:105-123** hardcodes `ctrl = true`:
```kotlin
// ❌ WRONG: Hardcoded Ctrl (doesn't work on macOS)
Item(
"New Note",
shortcut = KeyShortcut(Key.N, ctrl = true), // Should be Cmd on macOS
onClick = { /* ... */ }
)
```
**Fix:**
```kotlin
// ✅ CORRECT: OS-aware
Item(
"New Note",
shortcut = DesktopShortcuts.primary(Key.N),
onClick = { /* ... */ }
)
```
---
## References
- [macOS Keyboard Shortcuts](https://support.apple.com/en-us/102650)
- [Windows Keyboard Shortcuts](https://support.microsoft.com/en-us/windows/keyboard-shortcuts-in-windows-dcc61a57-8ff0-cffe-9796-cb9706c75eec)
- [GNOME Keyboard Shortcuts](https://help.gnome.org/users/gnome-help/stable/shell-keyboard-shortcuts.html)
- [Material Design: Keyboard Shortcuts](https://m3.material.io/foundations/interaction/keyboard)
- [Compose Desktop: Keyboard Events](https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-desktop-keyboard.html)
@@ -0,0 +1,579 @@
# OS Detection & Platform-Specific Code
Patterns for detecting operating system and implementing platform-specific behavior in Compose Desktop.
## OS Detection
### Basic Detection
```kotlin
val osName = System.getProperty("os.name").lowercase()
val isMacOS = osName.contains("mac")
val isWindows = osName.contains("win")
val isLinux = osName.contains("nux") || osName.contains("nix")
```
### System Properties
```kotlin
// OS name
System.getProperty("os.name")
// Examples: "Mac OS X", "Windows 10", "Linux"
// OS version
System.getProperty("os.version")
// Examples: "14.2.1", "10.0", "6.5.0-14-generic"
// OS architecture
System.getProperty("os.arch")
// Examples: "aarch64", "x86_64", "amd64"
// User home directory
System.getProperty("user.home")
// Examples: "/Users/username", "C:\Users\username", "/home/username"
// File separator
System.getProperty("file.separator")
// Examples: "/" (Unix), "\" (Windows)
// Path separator
System.getProperty("path.separator")
// Examples: ":" (Unix), ";" (Windows)
```
---
## PlatformDetector Utility
Create a centralized utility for platform detection.
**File:** `commons/src/jvmMain/kotlin/utils/PlatformDetector.kt`
```kotlin
package com.vitorpamplona.amethyst.commons.utils
object PlatformDetector {
private val osName = System.getProperty("os.name").lowercase()
val isMacOS: Boolean = osName.contains("mac")
val isWindows: Boolean = osName.contains("win")
val isLinux: Boolean = osName.contains("nux") || osName.contains("nix")
val platform: Platform = when {
isMacOS -> Platform.MacOS
isWindows -> Platform.Windows
isLinux -> Platform.Linux
else -> Platform.Unknown
}
enum class Platform {
MacOS,
Windows,
Linux,
Unknown
}
// File paths
val fileSeparator: String = System.getProperty("file.separator")
val pathSeparator: String = System.getProperty("path.separator")
// User directories
val userHome: String = System.getProperty("user.home")
val appDataDir: String = when (platform) {
Platform.MacOS -> "$userHome/Library/Application Support"
Platform.Windows -> System.getenv("APPDATA") ?: "$userHome\\AppData\\Roaming"
Platform.Linux -> System.getenv("XDG_CONFIG_HOME") ?: "$userHome/.config"
Platform.Unknown -> userHome
}
// Modifier key names
val primaryModifierName: String = if (isMacOS) "Cmd" else "Ctrl"
val secondaryModifierName: String = if (isMacOS) "Option" else "Alt"
fun platformSpecific(
macOS: () -> Unit = {},
windows: () -> Unit = {},
linux: () -> Unit = {},
fallback: () -> Unit = {}
) {
when (platform) {
Platform.MacOS -> macOS()
Platform.Windows -> windows()
Platform.Linux -> linux()
Platform.Unknown -> fallback()
}
}
}
```
**Usage:**
```kotlin
// Simple check
if (PlatformDetector.isMacOS) {
// macOS-specific code
}
// Pattern matching
when (PlatformDetector.platform) {
Platform.MacOS -> setupMacDock()
Platform.Windows -> setupWindowsTray()
Platform.Linux -> setupLinuxTray()
Platform.Unknown -> showWarning()
}
// Platform-specific execution
PlatformDetector.platformSpecific(
macOS = { setupMacMenuBar() },
windows = { setupWindowsMenu() },
linux = { setupLinuxMenu() }
)
// File paths
val configPath = "${PlatformDetector.appDataDir}${PlatformDetector.fileSeparator}amethyst"
```
---
## Platform-Specific UI
### Keyboard Shortcuts Helper
```kotlin
package com.vitorpamplona.amethyst.commons.utils
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyShortcut
object DesktopShortcuts {
private val isMacOS = PlatformDetector.isMacOS
fun primary(key: Key) = if (isMacOS) {
KeyShortcut(key, meta = true)
} else {
KeyShortcut(key, ctrl = true)
}
fun primaryShift(key: Key) = if (isMacOS) {
KeyShortcut(key, meta = true, shift = true)
} else {
KeyShortcut(key, ctrl = true, shift = true)
}
fun primaryAlt(key: Key) = if (isMacOS) {
KeyShortcut(key, meta = true, alt = true)
} else {
KeyShortcut(key, ctrl = true, alt = true)
}
val modifierName = PlatformDetector.primaryModifierName
val secondaryName = PlatformDetector.secondaryModifierName
fun formatShortcut(key: String, withPrimary: Boolean = true): String {
return if (withPrimary) "$modifierName+$key" else key
}
}
```
### File Paths Helper
```kotlin
package com.vitorpamplona.amethyst.commons.utils
import java.io.File
object FilePaths {
private val separator = PlatformDetector.fileSeparator
fun join(vararg parts: String): String {
return parts.joinToString(separator)
}
fun appConfig(appName: String): String {
return join(PlatformDetector.appDataDir, appName)
}
fun appCache(appName: String): String {
return when (PlatformDetector.platform) {
PlatformDetector.Platform.MacOS ->
join(PlatformDetector.userHome, "Library", "Caches", appName)
PlatformDetector.Platform.Windows ->
join(System.getenv("LOCALAPPDATA") ?: "${PlatformDetector.userHome}\\AppData\\Local", appName)
PlatformDetector.Platform.Linux ->
join(System.getenv("XDG_CACHE_HOME") ?: "${PlatformDetector.userHome}/.cache", appName)
else -> join(PlatformDetector.userHome, ".cache", appName)
}
}
fun ensureDirectory(path: String): File {
return File(path).apply {
if (!exists()) {
mkdirs()
}
}
}
}
// Usage
val configDir = FilePaths.ensureDirectory(FilePaths.appConfig("amethyst"))
val cacheDir = FilePaths.ensureDirectory(FilePaths.appCache("amethyst"))
```
---
## Platform-Specific Features
### Open External URL
```kotlin
// commons/src/commonMain/kotlin/utils/ExternalUrl.kt
expect fun openExternalUrl(url: String)
// commons/src/jvmMain/kotlin/utils/ExternalUrl.jvm.kt
import java.awt.Desktop
import java.net.URI
actual fun openExternalUrl(url: String) {
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
Desktop.getDesktop().browse(URI(url))
}
}
```
### File Picker
```kotlin
// Platform-specific file picker
fun showFilePicker(
title: String = "Select file",
mode: FilePickerMode = FilePickerMode.Load
): String? {
val fileDialog = java.awt.FileDialog(
java.awt.Frame(),
title,
when (mode) {
FilePickerMode.Load -> java.awt.FileDialog.LOAD
FilePickerMode.Save -> java.awt.FileDialog.SAVE
}
)
// macOS-specific: Enable file selection features
if (PlatformDetector.isMacOS) {
System.setProperty("apple.awt.fileDialogForDirectories", "false")
}
fileDialog.isVisible = true
return fileDialog.file?.let { "${fileDialog.directory}$it" }
}
enum class FilePickerMode {
Load,
Save
}
```
### Directory Picker (macOS)
```kotlin
fun showDirectoryPicker(title: String = "Select directory"): String? {
if (PlatformDetector.isMacOS) {
// macOS-specific directory picker
System.setProperty("apple.awt.fileDialogForDirectories", "true")
}
val fileDialog = java.awt.FileDialog(java.awt.Frame(), title, java.awt.FileDialog.LOAD)
fileDialog.isVisible = true
if (PlatformDetector.isMacOS) {
System.setProperty("apple.awt.fileDialogForDirectories", "false")
}
return fileDialog.directory
}
```
---
## Window Decorations
### macOS-Specific
```kotlin
// Unified title bar (macOS Big Sur+)
if (PlatformDetector.isMacOS) {
Window(
undecorated = false,
transparent = true,
// ...
) {
// Custom title bar
}
}
```
### Windows-Specific
```kotlin
// Custom window chrome (Windows)
if (PlatformDetector.isWindows) {
Window(
undecorated = true,
// Custom decorations
) {
Column {
// Custom title bar with min/max/close buttons
WindowTitleBar()
// Content
}
}
}
```
---
## System Tray Icons
Different icon formats per OS:
```kotlin
fun getTrayIcon(): Painter {
return when (PlatformDetector.platform) {
Platform.MacOS -> painterResource("tray-icon-mac.png") // Template icon
Platform.Windows -> painterResource("tray-icon-win.ico")
Platform.Linux -> painterResource("tray-icon-linux.png")
else -> painterResource("tray-icon.png")
}
}
// macOS: Template icons (black/transparent)
// Windows: ICO format, 16x16
// Linux: PNG, typically 24x24
```
---
## Native Notifications
```kotlin
// Platform-specific notification implementation
fun sendNotification(title: String, message: String) {
PlatformDetector.platformSpecific(
macOS = {
// macOS: Use NSUserNotification (via tray)
trayState.sendNotification(
Notification(title, message, Notification.Type.Info)
)
},
windows = {
// Windows: Use Windows toast notifications
trayState.sendNotification(
Notification(title, message, Notification.Type.Info)
)
},
linux = {
// Linux: Use libnotify (via tray)
trayState.sendNotification(
Notification(title, message, Notification.Type.Info)
)
}
)
}
```
---
## Architecture Detection
```kotlin
object ArchDetector {
private val arch = System.getProperty("os.arch").lowercase()
val isArm: Boolean = arch.contains("aarch") || arch.contains("arm")
val isX64: Boolean = arch.contains("x86_64") || arch.contains("amd64")
val isX86: Boolean = arch.contains("x86") && !isX64
val architecture: Architecture = when {
isArm -> Architecture.ARM
isX64 -> Architecture.X64
isX86 -> Architecture.X86
else -> Architecture.Unknown
}
enum class Architecture {
ARM,
X64,
X86,
Unknown
}
}
// Usage: Load correct native library
fun loadNativeLib() {
val libName = when {
PlatformDetector.isMacOS && ArchDetector.isArm -> "libsecp256k1-macos-arm64"
PlatformDetector.isMacOS && ArchDetector.isX64 -> "libsecp256k1-macos-x64"
PlatformDetector.isWindows && ArchDetector.isX64 -> "libsecp256k1-win-x64"
PlatformDetector.isLinux && ArchDetector.isX64 -> "libsecp256k1-linux-x64"
else -> throw UnsupportedOperationException("Unsupported platform")
}
System.loadLibrary(libName)
}
```
---
## Testing Platform Detection
```kotlin
@Test
fun testPlatformDetection() {
println("OS: ${System.getProperty("os.name")}")
println("Version: ${System.getProperty("os.version")}")
println("Arch: ${System.getProperty("os.arch")}")
println()
println("Is macOS: ${PlatformDetector.isMacOS}")
println("Is Windows: ${PlatformDetector.isWindows}")
println("Is Linux: ${PlatformDetector.isLinux}")
println("Platform: ${PlatformDetector.platform}")
println()
println("User home: ${PlatformDetector.userHome}")
println("App data: ${PlatformDetector.appDataDir}")
println("File separator: ${PlatformDetector.fileSeparator}")
}
// Example output (macOS):
// OS: Mac OS X
// Version: 14.2.1
// Arch: aarch64
//
// Is macOS: true
// Is Windows: false
// Is Linux: false
// Platform: MacOS
//
// User home: /Users/username
// App data: /Users/username/Library/Application Support
// File separator: /
```
---
## Best Practices
### 1. Centralize Detection
**DO:** Use PlatformDetector singleton
```kotlin
if (PlatformDetector.isMacOS) { /* ... */ }
```
**DON'T:** Repeat detection everywhere
```kotlin
if (System.getProperty("os.name").lowercase().contains("mac")) { /* ... */ }
```
### 2. Use expect/actual for Platform APIs
```kotlin
// commonMain
expect fun openFile(path: String)
// jvmMain (Desktop)
actual fun openFile(path: String) {
Desktop.getDesktop().open(File(path))
}
// androidMain
actual fun openFile(path: String) {
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(path)))
}
```
### 3. Graceful Degradation
```kotlin
fun openBrowser(url: String) {
try {
if (Desktop.isDesktopSupported()) {
Desktop.getDesktop().browse(URI(url))
} else {
// Fallback: Copy to clipboard
Toolkit.getDefaultToolkit().systemClipboard.setContents(
StringSelection(url),
null
)
showMessage("URL copied to clipboard: $url")
}
} catch (e: Exception) {
showError("Failed to open browser: ${e.message}")
}
}
```
### 4. Test on All Platforms
Always test platform-specific code on:
- macOS (Intel + Apple Silicon if possible)
- Windows (10/11)
- Linux (Ubuntu/Fedora)
---
## Common Patterns
### Pattern: Config File Location
```kotlin
fun getConfigFile(filename: String): File {
val configDir = when (PlatformDetector.platform) {
Platform.MacOS ->
File("${PlatformDetector.userHome}/Library/Application Support/Amethyst")
Platform.Windows ->
File("${System.getenv("APPDATA")}\\Amethyst")
Platform.Linux ->
File("${PlatformDetector.userHome}/.config/amethyst")
else ->
File("${PlatformDetector.userHome}/.amethyst")
}
if (!configDir.exists()) {
configDir.mkdirs()
}
return File(configDir, filename)
}
// Usage
val settingsFile = getConfigFile("settings.json")
```
### Pattern: Platform-Specific Resources
```kotlin
fun getPlatformIcon(name: String): Painter {
val extension = when (PlatformDetector.platform) {
Platform.MacOS -> "icns"
Platform.Windows -> "ico"
else -> "png"
}
return painterResource("$name.$extension")
}
// Resources:
// src/jvmMain/resources/app-icon.icns (macOS)
// src/jvmMain/resources/app-icon.ico (Windows)
// src/jvmMain/resources/app-icon.png (Linux)
```
---
## References
- [System Properties (Java)](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/System.html#getProperties())
- [Desktop API (Java)](https://docs.oracle.com/en/java/javase/21/docs/api/java.desktop/java/awt/Desktop.html)
- [File System Standards (XDG)](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html)