feat(export): add Tauri commands for transcript, task, and Obsidian export
Three new commands: export_transcriptions (MD/TXT), export_tasks (JSON/CSV), and export_to_obsidian (MD with wikilinks and checklists). All write plain text files to a user-selected directory. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -39,3 +39,4 @@ arboard = "3.6.1"
|
||||
tauri-plugin-mcp = "0.7.1"
|
||||
sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio"] }
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
chrono = "0.4"
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
{"default":{"identifier":"default","description":"Capability for the main window","local":true,"windows":["main","tasks-float"],"permissions":["core:default","core:window:allow-start-dragging","core:window:allow-set-always-on-top","core:window:allow-minimize","core:window:allow-toggle-maximize","core:window:allow-is-maximized","core:window:allow-close","core:window:allow-hide","core:window:allow-show","core:window:allow-set-focus","opener:default","dialog:default","global-shortcut:allow-register","global-shortcut:allow-unregister"]}}
|
||||
{"default":{"identifier":"default","description":"Capability for the main window","local":true,"windows":["main","tasks-float"],"permissions":["core:default","core:window:allow-start-dragging","core:window:allow-set-always-on-top","core:window:allow-minimize","core:window:allow-toggle-maximize","core:window:allow-is-maximized","core:window:allow-close","core:window:allow-hide","core:window:allow-show","core:window:allow-set-focus","opener:default","dialog:default","global-shortcut:allow-register","global-shortcut:allow-unregister","notification:default"]}}
|
||||
@@ -2498,6 +2498,204 @@
|
||||
"const": "mcp:deny-list-tools",
|
||||
"markdownDescription": "Denies the list_tools command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`",
|
||||
"type": "string",
|
||||
"const": "notification:default",
|
||||
"markdownDescription": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`"
|
||||
},
|
||||
{
|
||||
"description": "Enables the batch command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-batch",
|
||||
"markdownDescription": "Enables the batch command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the cancel command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-cancel",
|
||||
"markdownDescription": "Enables the cancel command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the check_permissions command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-check-permissions",
|
||||
"markdownDescription": "Enables the check_permissions command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the create_channel command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-create-channel",
|
||||
"markdownDescription": "Enables the create_channel command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the delete_channel command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-delete-channel",
|
||||
"markdownDescription": "Enables the delete_channel command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the get_active command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-get-active",
|
||||
"markdownDescription": "Enables the get_active command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the get_pending command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-get-pending",
|
||||
"markdownDescription": "Enables the get_pending command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the is_permission_granted command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-is-permission-granted",
|
||||
"markdownDescription": "Enables the is_permission_granted command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the list_channels command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-list-channels",
|
||||
"markdownDescription": "Enables the list_channels command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the notify command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-notify",
|
||||
"markdownDescription": "Enables the notify command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the permission_state command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-permission-state",
|
||||
"markdownDescription": "Enables the permission_state command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the register_action_types command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-register-action-types",
|
||||
"markdownDescription": "Enables the register_action_types command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the register_listener command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-register-listener",
|
||||
"markdownDescription": "Enables the register_listener command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the remove_active command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-remove-active",
|
||||
"markdownDescription": "Enables the remove_active command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the request_permission command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-request-permission",
|
||||
"markdownDescription": "Enables the request_permission command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the show command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-show",
|
||||
"markdownDescription": "Enables the show command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the batch command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-batch",
|
||||
"markdownDescription": "Denies the batch command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the cancel command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-cancel",
|
||||
"markdownDescription": "Denies the cancel command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the check_permissions command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-check-permissions",
|
||||
"markdownDescription": "Denies the check_permissions command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the create_channel command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-create-channel",
|
||||
"markdownDescription": "Denies the create_channel command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the delete_channel command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-delete-channel",
|
||||
"markdownDescription": "Denies the delete_channel command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the get_active command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-get-active",
|
||||
"markdownDescription": "Denies the get_active command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the get_pending command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-get-pending",
|
||||
"markdownDescription": "Denies the get_pending command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the is_permission_granted command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-is-permission-granted",
|
||||
"markdownDescription": "Denies the is_permission_granted command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the list_channels command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-list-channels",
|
||||
"markdownDescription": "Denies the list_channels command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the notify command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-notify",
|
||||
"markdownDescription": "Denies the notify command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the permission_state command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-permission-state",
|
||||
"markdownDescription": "Denies the permission_state command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the register_action_types command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-register-action-types",
|
||||
"markdownDescription": "Denies the register_action_types command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the register_listener command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-register-listener",
|
||||
"markdownDescription": "Denies the register_listener command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the remove_active command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-remove-active",
|
||||
"markdownDescription": "Denies the remove_active command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the request_permission command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-request-permission",
|
||||
"markdownDescription": "Denies the request_permission command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the show command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-show",
|
||||
"markdownDescription": "Denies the show command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`",
|
||||
"type": "string",
|
||||
|
||||
@@ -2498,6 +2498,204 @@
|
||||
"const": "mcp:deny-list-tools",
|
||||
"markdownDescription": "Denies the list_tools command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`",
|
||||
"type": "string",
|
||||
"const": "notification:default",
|
||||
"markdownDescription": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`"
|
||||
},
|
||||
{
|
||||
"description": "Enables the batch command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-batch",
|
||||
"markdownDescription": "Enables the batch command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the cancel command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-cancel",
|
||||
"markdownDescription": "Enables the cancel command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the check_permissions command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-check-permissions",
|
||||
"markdownDescription": "Enables the check_permissions command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the create_channel command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-create-channel",
|
||||
"markdownDescription": "Enables the create_channel command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the delete_channel command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-delete-channel",
|
||||
"markdownDescription": "Enables the delete_channel command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the get_active command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-get-active",
|
||||
"markdownDescription": "Enables the get_active command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the get_pending command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-get-pending",
|
||||
"markdownDescription": "Enables the get_pending command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the is_permission_granted command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-is-permission-granted",
|
||||
"markdownDescription": "Enables the is_permission_granted command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the list_channels command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-list-channels",
|
||||
"markdownDescription": "Enables the list_channels command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the notify command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-notify",
|
||||
"markdownDescription": "Enables the notify command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the permission_state command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-permission-state",
|
||||
"markdownDescription": "Enables the permission_state command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the register_action_types command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-register-action-types",
|
||||
"markdownDescription": "Enables the register_action_types command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the register_listener command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-register-listener",
|
||||
"markdownDescription": "Enables the register_listener command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the remove_active command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-remove-active",
|
||||
"markdownDescription": "Enables the remove_active command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the request_permission command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-request-permission",
|
||||
"markdownDescription": "Enables the request_permission command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the show command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-show",
|
||||
"markdownDescription": "Enables the show command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the batch command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-batch",
|
||||
"markdownDescription": "Denies the batch command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the cancel command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-cancel",
|
||||
"markdownDescription": "Denies the cancel command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the check_permissions command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-check-permissions",
|
||||
"markdownDescription": "Denies the check_permissions command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the create_channel command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-create-channel",
|
||||
"markdownDescription": "Denies the create_channel command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the delete_channel command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-delete-channel",
|
||||
"markdownDescription": "Denies the delete_channel command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the get_active command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-get-active",
|
||||
"markdownDescription": "Denies the get_active command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the get_pending command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-get-pending",
|
||||
"markdownDescription": "Denies the get_pending command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the is_permission_granted command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-is-permission-granted",
|
||||
"markdownDescription": "Denies the is_permission_granted command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the list_channels command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-list-channels",
|
||||
"markdownDescription": "Denies the list_channels command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the notify command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-notify",
|
||||
"markdownDescription": "Denies the notify command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the permission_state command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-permission-state",
|
||||
"markdownDescription": "Denies the permission_state command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the register_action_types command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-register-action-types",
|
||||
"markdownDescription": "Denies the register_action_types command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the register_listener command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-register-listener",
|
||||
"markdownDescription": "Denies the register_listener command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the remove_active command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-remove-active",
|
||||
"markdownDescription": "Denies the remove_active command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the request_permission command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-request-permission",
|
||||
"markdownDescription": "Denies the request_permission command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the show command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-show",
|
||||
"markdownDescription": "Denies the show command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`",
|
||||
"type": "string",
|
||||
|
||||
282
src-tauri/src/commands/export.rs
Normal file
282
src-tauri/src/commands/export.rs
Normal file
@@ -0,0 +1,282 @@
|
||||
use std::path::Path;
|
||||
|
||||
use crate::AppState;
|
||||
use kon_storage::{list_subtasks, list_tasks, list_transcripts, TaskRow, TranscriptRow};
|
||||
|
||||
/// Slugify a string for use in filenames.
|
||||
fn slugify(s: &str) -> String {
|
||||
s.to_lowercase()
|
||||
.chars()
|
||||
.map(|c| if c.is_alphanumeric() { c } else { '-' })
|
||||
.collect::<String>()
|
||||
.split('-')
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join("-")
|
||||
}
|
||||
|
||||
/// Parse a `created_at` timestamp into a filename-safe prefix.
|
||||
/// Expected format: "YYYY-MM-DD HH:MM:SS" → "YYYY-MM-DD_HH-MM-SS"
|
||||
fn timestamp_to_filename(created_at: &str) -> String {
|
||||
created_at
|
||||
.replace(' ', "_")
|
||||
.replace(':', "-")
|
||||
}
|
||||
|
||||
/// Build Markdown content for a single transcription with YAML frontmatter.
|
||||
fn transcription_to_markdown(t: &TranscriptRow) -> String {
|
||||
let title = t.title.as_deref().unwrap_or("Untitled");
|
||||
let word_count = t.text.split_whitespace().count();
|
||||
format!(
|
||||
"---\ntitle: \"{}\"\ndate: \"{}\"\nduration_seconds: {}\nword_count: {}\n---\n\n{}",
|
||||
title, t.created_at, t.duration, word_count, t.text
|
||||
)
|
||||
}
|
||||
|
||||
/// Build Obsidian-compatible Markdown for a transcription.
|
||||
fn transcription_to_obsidian(t: &TranscriptRow, task_titles: &[String]) -> String {
|
||||
let title = t.title.as_deref().unwrap_or("Untitled");
|
||||
let mut md = format!(
|
||||
"---\ntitle: \"{}\"\ndate: \"{}\"\nduration: {}\ntags: [kon, transcription]\n---\n\n{}",
|
||||
title, t.created_at, t.duration, t.text
|
||||
);
|
||||
|
||||
if !task_titles.is_empty() {
|
||||
md.push_str("\n\n## Extracted Tasks\n\n");
|
||||
for title in task_titles {
|
||||
md.push_str(&format!("- [[{}]]\n", title));
|
||||
}
|
||||
}
|
||||
|
||||
md
|
||||
}
|
||||
|
||||
/// Build Obsidian-compatible Markdown for a task.
|
||||
fn task_to_obsidian(task: &TaskRow, subtasks: &[TaskRow]) -> String {
|
||||
let title = &task.text;
|
||||
let mut md = format!(
|
||||
"---\ntitle: \"{}\"\nstatus: \"{}\"\npriority: \"{}\"\nproject: \"{}\"\ncreated: \"{}\"\nsource_transcription: \"{}\"\n---\n\n{}",
|
||||
title,
|
||||
task.status,
|
||||
task.priority,
|
||||
task.project.as_deref().unwrap_or(""),
|
||||
task.created_at,
|
||||
task.source_transcript_id.as_deref().unwrap_or(""),
|
||||
task.text,
|
||||
);
|
||||
|
||||
if !subtasks.is_empty() {
|
||||
md.push_str("\n\n## Micro-steps\n\n");
|
||||
for st in subtasks {
|
||||
let check = if st.done { "x" } else { " " };
|
||||
md.push_str(&format!("- [{}] {}\n", check, st.text));
|
||||
}
|
||||
}
|
||||
|
||||
md
|
||||
}
|
||||
|
||||
/// Build a CSV row, escaping fields that contain commas or quotes.
|
||||
fn csv_escape(field: &str) -> String {
|
||||
if field.contains(',') || field.contains('"') || field.contains('\n') {
|
||||
format!("\"{}\"", field.replace('"', "\"\""))
|
||||
} else {
|
||||
field.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Export all transcriptions as individual files (Markdown or plain text).
|
||||
#[tauri::command]
|
||||
pub async fn export_transcriptions(
|
||||
state: tauri::State<'_, AppState>,
|
||||
directory: String,
|
||||
format: String,
|
||||
) -> Result<u32, String> {
|
||||
let dir = Path::new(&directory);
|
||||
if !dir.exists() {
|
||||
return Err(format!("Directory does not exist: {}", directory));
|
||||
}
|
||||
|
||||
let transcripts = list_transcripts(&state.db, i64::MAX)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let mut count = 0u32;
|
||||
for t in &transcripts {
|
||||
let prefix = timestamp_to_filename(&t.created_at);
|
||||
let (ext, content) = match format.as_str() {
|
||||
"txt" => ("txt", t.text.clone()),
|
||||
_ => ("md", transcription_to_markdown(t)),
|
||||
};
|
||||
let filename = format!("{}_transcription.{}", prefix, ext);
|
||||
let path = dir.join(&filename);
|
||||
std::fs::write(&path, &content)
|
||||
.map_err(|e| format!("Failed to write {}: {}", filename, e))?;
|
||||
count += 1;
|
||||
}
|
||||
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
/// Export all top-level tasks with subtasks as JSON or CSV.
|
||||
#[tauri::command]
|
||||
pub async fn export_tasks(
|
||||
state: tauri::State<'_, AppState>,
|
||||
directory: String,
|
||||
format: String,
|
||||
) -> Result<String, String> {
|
||||
let dir = Path::new(&directory);
|
||||
if !dir.exists() {
|
||||
return Err(format!("Directory does not exist: {}", directory));
|
||||
}
|
||||
|
||||
let all_tasks = list_tasks(&state.db).await.map_err(|e| e.to_string())?;
|
||||
let top_level: Vec<&TaskRow> = all_tasks.iter().filter(|t| t.parent_task_id.is_none()).collect();
|
||||
|
||||
let today = chrono::Local::now().format("%Y-%m-%d").to_string();
|
||||
let (filename, content) = match format.as_str() {
|
||||
"csv" => {
|
||||
let mut csv = String::from("id,text,priority,project,status,bucket,effort,done,done_at,created_at,parent_task_id\n");
|
||||
for task in &top_level {
|
||||
csv.push_str(&format!(
|
||||
"{},{},{},{},{},{},{},{},{},{},\n",
|
||||
csv_escape(&task.id),
|
||||
csv_escape(&task.text),
|
||||
csv_escape(&task.priority),
|
||||
csv_escape(task.project.as_deref().unwrap_or("")),
|
||||
csv_escape(&task.status),
|
||||
csv_escape(&task.bucket),
|
||||
csv_escape(task.effort.as_deref().unwrap_or("")),
|
||||
task.done,
|
||||
csv_escape(task.done_at.as_deref().unwrap_or("")),
|
||||
csv_escape(&task.created_at),
|
||||
));
|
||||
// Include subtasks
|
||||
let subs = list_subtasks(&state.db, &task.id)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
for st in &subs {
|
||||
csv.push_str(&format!(
|
||||
"{},{},{},{},{},{},{},{},{},{},{}\n",
|
||||
csv_escape(&st.id),
|
||||
csv_escape(&st.text),
|
||||
csv_escape(&st.priority),
|
||||
csv_escape(st.project.as_deref().unwrap_or("")),
|
||||
csv_escape(&st.status),
|
||||
csv_escape(&st.bucket),
|
||||
csv_escape(st.effort.as_deref().unwrap_or("")),
|
||||
st.done,
|
||||
csv_escape(st.done_at.as_deref().unwrap_or("")),
|
||||
csv_escape(&st.created_at),
|
||||
csv_escape(&task.id),
|
||||
));
|
||||
}
|
||||
}
|
||||
(format!("kon-tasks-{}.csv", today), csv)
|
||||
}
|
||||
_ => {
|
||||
// JSON format
|
||||
let mut tasks_json = Vec::new();
|
||||
for task in &top_level {
|
||||
let subs = list_subtasks(&state.db, &task.id)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let subtasks_json: Vec<serde_json::Value> = subs
|
||||
.iter()
|
||||
.map(|st| {
|
||||
serde_json::json!({
|
||||
"id": st.id,
|
||||
"text": st.text,
|
||||
"priority": st.priority,
|
||||
"project": st.project,
|
||||
"status": st.status,
|
||||
"bucket": st.bucket,
|
||||
"effort": st.effort,
|
||||
"done": st.done,
|
||||
"done_at": st.done_at,
|
||||
"created_at": st.created_at,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
tasks_json.push(serde_json::json!({
|
||||
"id": task.id,
|
||||
"text": task.text,
|
||||
"priority": task.priority,
|
||||
"project": task.project,
|
||||
"status": task.status,
|
||||
"bucket": task.bucket,
|
||||
"effort": task.effort,
|
||||
"done": task.done,
|
||||
"done_at": task.done_at,
|
||||
"created_at": task.created_at,
|
||||
"notes": task.notes,
|
||||
"source_transcript_id": task.source_transcript_id,
|
||||
"subtasks": subtasks_json,
|
||||
}));
|
||||
}
|
||||
let json_str = serde_json::to_string_pretty(&tasks_json)
|
||||
.map_err(|e| format!("JSON serialization failed: {}", e))?;
|
||||
(format!("kon-tasks-{}.json", today), json_str)
|
||||
}
|
||||
};
|
||||
|
||||
let path = dir.join(&filename);
|
||||
std::fs::write(&path, &content)
|
||||
.map_err(|e| format!("Failed to write {}: {}", filename, e))?;
|
||||
|
||||
Ok(path.to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
/// Export transcriptions and tasks as Obsidian-compatible Markdown.
|
||||
#[tauri::command]
|
||||
pub async fn export_to_obsidian(
|
||||
state: tauri::State<'_, AppState>,
|
||||
directory: String,
|
||||
) -> Result<u32, String> {
|
||||
let dir = Path::new(&directory);
|
||||
if !dir.exists() {
|
||||
return Err(format!("Directory does not exist: {}", directory));
|
||||
}
|
||||
|
||||
let transcripts = list_transcripts(&state.db, i64::MAX)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let all_tasks = list_tasks(&state.db).await.map_err(|e| e.to_string())?;
|
||||
let top_level: Vec<&TaskRow> = all_tasks.iter().filter(|t| t.parent_task_id.is_none()).collect();
|
||||
|
||||
let mut count = 0u32;
|
||||
|
||||
// Export transcriptions
|
||||
for t in &transcripts {
|
||||
// Find tasks sourced from this transcription
|
||||
let linked_titles: Vec<String> = top_level
|
||||
.iter()
|
||||
.filter(|task| task.source_transcript_id.as_deref() == Some(&t.id))
|
||||
.map(|task| task.text.clone())
|
||||
.collect();
|
||||
|
||||
let content = transcription_to_obsidian(t, &linked_titles);
|
||||
let prefix = timestamp_to_filename(&t.created_at);
|
||||
let filename = format!("{}_transcription.md", prefix);
|
||||
let path = dir.join(&filename);
|
||||
std::fs::write(&path, &content)
|
||||
.map_err(|e| format!("Failed to write {}: {}", filename, e))?;
|
||||
count += 1;
|
||||
}
|
||||
|
||||
// Export tasks
|
||||
for task in &top_level {
|
||||
let subs = list_subtasks(&state.db, &task.id)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let content = task_to_obsidian(task, &subs);
|
||||
let slug = slugify(&task.text);
|
||||
let filename = format!("task-{}.md", slug);
|
||||
let path = dir.join(&filename);
|
||||
std::fs::write(&path, &content)
|
||||
.map_err(|e| format!("Failed to write {}: {}", filename, e))?;
|
||||
count += 1;
|
||||
}
|
||||
|
||||
Ok(count)
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod audio;
|
||||
pub mod clipboard;
|
||||
pub mod export;
|
||||
pub mod hardware;
|
||||
pub mod history;
|
||||
pub mod llm;
|
||||
|
||||
@@ -192,6 +192,10 @@ pub fn run() {
|
||||
// Hardware
|
||||
commands::hardware::probe_system,
|
||||
commands::hardware::rank_models,
|
||||
// Export
|
||||
commands::export::export_transcriptions,
|
||||
commands::export::export_tasks,
|
||||
commands::export::export_to_obsidian,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running Kon");
|
||||
|
||||
Reference in New Issue
Block a user