Memanfaatkan native & system APIs untuk integrasi yang dalam: akses file system, system tray, notifikasi, dan fitur deep integration dengan OS, lengkap dengan praktik integrasi sistem untuk CatatanKu di tiga framework

Sejauh ini CatatanKu berjalan di dalam jendela aplikasi — UI yang berisi list dan editor. Aplikasi desktop sejati, bagaimanapun, tidak terkurung di jendelanya. Ia bisa muncul di system tray, mengirim notifikasi sistem, membuka file dari file explorer, dan mendaftarkan diri sebagai aplikasi bawaan untuk jenis file tertentu. Inilah native & system APIs: kemampuan yang tidak bisa ditiru aplikasi web, dan alasan utama aplikasi desktop tetap relevan (episode 1).
Episode ini membangun integrasi sistem untuk CatatanKu: akses file system yang aman, tray icon dengan menu, notifikasi saat sinkronisasi selesai, dan deep integration seperti file association. Prinsip yang kita pakai berlaku lintas framework — kita lihat praktiknya di Tauri, Electron, dan Flutter.
Backend process (Rust/Node/native) bisa mengakses filesystem — tetapi akses harus disengaja dan dibatasi, bukan izin bebas. CatatanKu perlu membaca file .txt, menyimpan pengaturan, dan mengekspor catatan:
use std::fs;
use tauri::{AppHandle, Manager};
use tauri_plugin_fs::FsExt;
#[tauri::command]
fn read_note(app: AppHandle, path: String) -> Result<String, String> {
let base = app.path().app_data_dir().map_err(|e| e.to_string())?;
let full = base.join(path);
if !full.starts_with(&base) {
return Err("path di luar app data dir".into());
}
fs::read_to_string(full).map_err(|e| e.to_string())
}const { app } = require("electron");
const fs = require("node:fs/promises");
const path = require("node:path");
async function readNote(fileName) {
const base = app.getPath("userData");
const full = path.join(base, "notes", fileName);
if (!full.startsWith(path.join(base, "notes"))) {
throw new Error("path di luar folder notes");
}
return fs.readFile(full, "utf-8");
}Dua hal penting yang sama di kedua contoh: simpan data di direktori aplikasi (app_data_dir / userData), dan validasi path agar tidak keluar dari folder tersebut — mencegah path traversal.
Warning
Jangan pernah mengizinkan UI mengirim path absolut bebas ke backend. Simpan path relatif terhadap direktori aplikasi, lalu validasi di backend. Prinsip ini (path sanitization) mencegah celah klasik path traversal yang sering muncul di aplikasi desktop.
Aplikasi seperti CatatanKu sering bekerja di latar belakang — sinkronisasi, pengindeksan — dan pengguna ingin mengaksesnya cepat tanpa membuka jendela. System tray (notification area di Windows, menu bar di macOS) adalah jawabannya:
use tauri::menu::{Menu, MenuItem};
use tauri::tray::TrayIconBuilder;
let open = MenuItem::with_id(app, "open", "Buka CatatanKu", true, None::<&str>)?;
let quit = MenuItem::with_id(app, "quit", "Keluar", true, None::<&str>)?;
let menu = Menu::with_items(app, &[&open, &quit])?;
TrayIconBuilder::new()
.icon(app.default_window_icon().unwrap().clone())
.menu(&menu)
.on_menu_event(|app, event| match event.id().as_ref() {
"open" => { /* tampilkan jendela */ }
"quit" => app.exit(0),
_ => {}
})
.build(app)?;const { Tray, Menu, nativeImage, app } = require("electron");
let tray = new Tray(nativeImage.createFromPath("icon.png"));
const contextMenu = Menu.buildFromTemplate([
{ label: "Buka CatatanKu", click: () => mainWindow.show() },
{ label: "Keluar", click: () => app.quit() },
]);
tray.setToolTip("CatatanKu");
tray.setContextMenu(contextMenu);Perhatikan pola: window utama bisa ditutup tanpa menghentikan aplikasi (di tray), dan dibuka kembali dari menu tray. Ini mengubah lifecycle app — kita perlu mengatur perilaku close di episode 24 (multi-window) dan 13 (auto-update).
Notifikasi OS adalah cara standar memberi tahu pengguna: sinkronisasi selesai, error terjadi, atau ada reminder. Di semua framework ada plugin/API khusus:
use tauri_plugin_notification::NotificationExt;
app.notification()
.builder()
.title("CatatanKu")
.body("Sinkronisasi selesai: 12 catatan diperbarui")
.show()?;const { Notification } = require("electron");
new Notification({
title: "CatatanKu",
body: "Sinkronisasi selesai: 12 catatan diperbarui",
}).show();Di Flutter, notifikasi desktop dipakai lewat package seperti flutter_local_notifications. Kunci dari ketiganya: notifikasi adalah event dari backend, dikirim saat peristiwa sistem terjadi — bukan sesuatu yang dipicu UI secara langsung.
Agar CatatanKu bisa dibuka dengan klik ganda pada file .md/.txt, aplikasi harus mendaftarkan diri sebagai handler jenis file tersebut. Ini dideklarasikan saat packaging:
{
"bundle": {
"fileAssociations": [
{
"ext": ["txt", "md"],
"name": "Dokumen CatatanKu",
"mimeType": "text/markdown"
}
]
}
}{
"build": {
"fileAssociations": [
{ "ext": ["txt", "md"], "name": "Dokumen CatatanKu" }
]
}
}Saat file dibuka, OS memanggil aplikasi dengan path file tersebut — aplikasi membaca argumen/payload dan membuka editor. Deep integration lainnya: drag-and-drop file dari explorer, global shortcut (misal Ctrl+Shift+Space untuk menampilkan CatatanKu dari mana pun), dan single instance lock agar tidak membuka dua proses yang sama.
Note
File association dan drag-and-drop adalah contoh "desktop-only" yang sulit atau mustahil di web. Inilah mengapa aplikasi seperti Obsidian, Notion Desktop, dan editor kode tetap membangun versi desktop — integrasi dengan OS pengguna adalah bagian dari produknya.
Rangkuman integrasi yang kita bangun di episode ini:
app_data_dir/userData dengan path sanitization..txt/.md dengan klik ganda.Inti yang harus dibawa pulang:
Di episode 8 selanjutnya kita akan membangun fondasi data CatatanKu: offline & local data — SQLite, penyimpanan konfigurasi, dan pola offline-first yang membuat catatan tersimpan lokal dan cepat. Sampai jumpa di episode 8!