@pheem49/mint 1.8.2 โ†’ 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/Cargo.toml +1 -1
  2. package/Release_Note.md +48 -3
  3. package/crates/mint-cli/src/agent.rs +95 -84
  4. package/crates/mint-cli/src/image.rs +8 -8
  5. package/crates/mint-cli/src/main.rs +806 -403
  6. package/crates/mint-cli/src/onboard.rs +108 -108
  7. package/crates/mint-cli/src/setup.rs +143 -143
  8. package/crates/mint-core/src/api_server.rs +57 -54
  9. package/crates/mint-core/src/browser.rs +1 -1
  10. package/crates/mint-core/src/chat.rs +123 -8
  11. package/crates/mint-core/src/code_tools.rs +18 -20
  12. package/crates/mint-core/src/config.rs +184 -17
  13. package/crates/mint-core/src/image_gen.rs +5 -5
  14. package/crates/mint-core/src/lib.rs +5 -5
  15. package/crates/mint-core/src/memory.rs +6 -1
  16. package/crates/mint-core/src/orchestration.rs +104 -85
  17. package/crates/mint-core/src/pictures.rs +3 -4
  18. package/crates/mint-core/src/semantic.rs +1 -1
  19. package/crates/mint-core/src/shell.rs +1 -1
  20. package/crates/mint-core/src/skills.rs +46 -19
  21. package/package.json +1 -1
  22. package/shared-modules/linux-audio/fluidsynth2-static.json +9 -3
  23. package/shared-modules/linux-audio/fluidsynth2.json +8 -2
  24. package/shared-modules/linux-audio/lilv.json +26 -19
  25. package/shared-modules/linux-audio/lv2.json +0 -1
  26. package/src/renderer/shared/components/AgentActivityTable.tsx +25 -6
  27. package/src/renderer/shared/utils/fileIcons.ts +135 -0
  28. package/src/renderer/src/components/ChatPanel.tsx +25 -5
  29. package/src/renderer/src/components/MintDashboard.tsx +45 -2
  30. package/src/renderer/src/components/Settings/AgentsTab.tsx +21 -2
  31. package/src/renderer/src/components/Settings/GeneralTab.tsx +256 -2
  32. package/src/renderer/src/components/SettingsWindow.tsx +68 -1
  33. package/src/renderer/src/components/WorkspacePanel.tsx +7 -125
  34. package/src/renderer/src/css/styles.css +16 -0
  35. package/src/renderer/src-web/components/ChatPanel.tsx +25 -5
  36. package/src/renderer/src-web/components/MintDashboard.tsx +19 -1
  37. package/src/renderer/src-web/components/Settings/AgentsTab.tsx +21 -2
  38. package/src/renderer/src-web/components/Settings/GeneralTab.tsx +254 -3
  39. package/src/renderer/src-web/components/SettingsWindow.tsx +75 -1
  40. package/src/renderer/src-web/css/styles.css +16 -0
  41. package/src-tauri/src/desktop.rs +18 -16
  42. package/src-tauri/src/discord_rpc.rs +1 -1
  43. package/src-tauri/src/lib.rs +41 -17
  44. package/src-tauri/tauri.conf.json +1 -1
package/Cargo.toml CHANGED
@@ -7,7 +7,7 @@ members = [
7
7
  resolver = "2"
8
8
 
9
9
  [workspace.package]
10
- version = "1.8.2"
10
+ version = "1.9.0"
11
11
  edition = "2024"
12
12
  license = "AGPL-3.0-only"
13
13
 
package/Release_Note.md CHANGED
@@ -1,10 +1,32 @@
1
- # Release Notes - Mint Agent v1.8.2
1
+ # Release Notes - Mint Agent v1.9.0
2
2
 
3
- We are excited to release **Mint Agent v1.8.2**! This release introduces a configurable Multi-Agent system for sequential collaboration, powerful local workspace file management features, real-time AI-generated workflow suggestions in the user's active language, mobile layout optimization, and general UI/UX polish.
3
+ We are excited to release **Mint Agent v1.9.0**! This release introduces a configurable Multi-Agent system for sequential collaboration, powerful local workspace file management features, real-time AI-generated workflow suggestions in the user's active language, mobile layout optimization, and general UI/UX polish.
4
+
5
+ ---
6
+
7
+ ## ๐Ÿ”Œ Custom Provider Support
8
+
9
+ Users can now add any **OpenAI-compatible endpoint** as a fully configurable custom AI provider, giving complete control over which LLM backend Mint uses.
10
+
11
+ ### Backend (`config.rs`, `chat.rs`)
12
+ - **New data model:** Added `CustomProvider`, `CustomProviderModel`, and `CustomProviderHeader` structs to `MintConfig`. Custom providers are persisted to `mint-config.json` under the `customProviders` key.
13
+ - **Provider ID format:** Each custom provider is stored with a user-defined slug ID and is referenced in `ai_provider` as `"custom:<id>"` (e.g. `"custom:myprovider"`).
14
+ - **Chat routing:** `send_chat()` and `stream_chat()` now dispatch `custom:*` providers through `call_custom_provider()` / `stream_custom_provider()`, which use the existing OpenAI-compatible HTTP code path.
15
+ - **Multiple models:** Each provider stores a list of models; the active model is resolved from `customModelSelections` extra config (set by the UI model selector dropdown).
16
+ - **Custom HTTP headers:** All configured headers are applied to every request sent to that provider.
17
+ - **`available_providers()`** now returns `Vec<String>` (up from `Vec<&'static str>`) to accommodate dynamic custom provider IDs.
18
+ - **`resolve_custom_provider()`** helper added to look up a `CustomProvider` by key (strips the `custom:` prefix automatically).
19
+
20
+ ### Frontend (`SettingsWindow.tsx`, `GeneralTab.tsx`, `AgentsTab.tsx`)
21
+ - **Settings โ†’ General โ†’ Active Provider dropdown** now includes all configured custom providers dynamically.
22
+ - **Model selector:** When a custom provider is selected as the active provider, a second dropdown appears to pick among that provider's configured models.
23
+ - **Custom Providers section:** A new section at the bottom of General settings displays each custom provider as a card. Each card allows inline editing of: Provider ID (slug-validated), Display Name, Base URL, API Key (password), per-model rows (model ID + display name), and per-header rows (name + value). Providers can be added with `+ Add custom provider` and deleted inline.
24
+ - **Agents tab:** Custom providers (with a configured base URL) now appear in the AI Provider dropdown when creating or editing an agent. The model selector shows that provider's configured models.
4
25
 
5
26
  ---
6
27
 
7
28
  ## ๐Ÿ” Web Search Source Cards
29
+
8
30
  - **Favicon Source Cards:** After a web search, compact clickable source cards now appear above the AI response bubble โ€” each showing the website's favicon, domain name, and a tooltip with the page snippet. Clicking a card opens the source in the browser.
9
31
  - **Frontend-only change:** Parsed directly from existing `AgentProgress` `ToolEnd` events โ€” no backend changes required.
10
32
  - The AI response also includes a plain-text `Sources:` section listing title and URL for each result used.
@@ -37,9 +59,13 @@ Improved the autocomplete experience in the terminal:
37
59
  - **Simplified Exit Options:** Removed `/quit` from the autocomplete suggestions and help output (while retaining hidden execution support) to clean up the interface.
38
60
  - **Real-Time Agent & Model Status:** The CLI live thinking status bar dynamically displays which agent and model is executing.
39
61
 
40
- ### ๐Ÿ“‹ 5. Fix Clipboard Permission Popup on Text Paste (Ctrl+V)
62
+ ### ๐Ÿ“‹ 5. Fix Clipboard Permission Popup on Text Paste (Ctrl+V) & Desktop Image Paste Fallback
41
63
  - Fixed a bug where pasting text (or pressing `Ctrl + V`) would trigger a browser-level clipboard permission warning popup (asking to "Allow Paste").
42
64
  - Removed the redundant global `keydown` Ctrl+V listener and the async `navigator.clipboard.read()` fallback on text paste events. The application now correctly relies on standard, synchronous `event.clipboardData` values during paste events, ensuring seamless pasting of both images and text without prompting the user.
65
+ - **Tauri Native Fallback:** Restored an asynchronous fallback using Tauri's native backend `readClipboardImage` command specifically for the desktop application. This solves the Linux/Windows WebView limitations where WebKitGTK and standard web APIs fail to expose file handles in standard paste events.
66
+ - **Double Paste Throttling:** Implemented a 100ms throttle wrapper around the native clipboard read callback, preventing duplicate image attachments caused by concurrent window-level capture and element-level paste event propagation.
67
+
68
+
43
69
 
44
70
  ### ๐Ÿ“ฆ 6. Codebase Refactoring & Shared Utilities
45
71
  - **Shared UI & Progress Helpers:** Refactored duplicate helper functions and TypeScript interfaces from the desktop renderer (`src/renderer/src`) and web renderer (`src/renderer/src-web`) into the unified `shared` codebase.
@@ -68,8 +94,16 @@ Improved the autocomplete experience in the terminal:
68
94
  ### ๐Ÿค– 11. AI Model Self-Awareness Context Injection
69
95
  - **Dynamic Context Injection**: Updated the core prompt compilation layer to automatically append an `[Active Environment Context]` block to the system instructions. This supplies the active AI model name and provider to the AI model itself, allowing the AI to successfully know its own model configurations in real-time when queried by the user.
70
96
 
97
+ ### ๐ŸŽจ 12. Material Icon Theme in Agent Activity Tracker
98
+ - **Visual File & Folder Tracking**: The agent activity tracker (e.g. "Exploring file" progress log) now resolves target filenames/foldernames to display specific Material Icon Theme SVGs (e.g. JavaScript icon for `.js`, Rust icon for `.rs` / Cargo directories). This replaces the generic wireframe outline icons, resulting in a cleaner and more polished UI.
99
+
100
+ ### ๐ŸŽ™๏ธ 13. Stricter Voice Reply Settings Control
101
+ - **Strict Control Switch**: The global "Enable Voice Reply" toggle now acts as a strict master switch. When disabled, Mint will never speak responses out loud under any circumstances (including when using voice conversation/microphone mode).
102
+ - **Voice Mode Loop Resumption**: When "Enable Voice Reply" is disabled but voice conversation mode is active, the microphone automatically resumes listening after the AI response is printed on screen, maintaining the continuous hands-free voice loop.
103
+
71
104
  ---
72
105
 
106
+
73
107
  ## ๐Ÿ› ๏ธ Codebase Changes
74
108
 
75
109
  ### Tauri Backend (`src-tauri` & `crates/mint-core`)
@@ -111,6 +145,10 @@ Improved the autocomplete experience in the terminal:
111
145
  - Refactor duplicated types (`DiffHunk`, `FileChange`), helper functions (`numericSetting`, `errorMessage`, `readImage`, `readDocument`, `createTrimmedImagePreview`, `lightenColor`, `hexToRgb`, `applyThemeStyles`), and progress parsers (`parseFileChangesFromProgress`) from the desktop and web components to a central `src/renderer/shared` repository. Corrected a type checking issue by exporting `AgentProgress` and `InteractionMemory` from `shared/agentProgress.ts`.
112
146
  - Consolidate microphone interface to client-side Speech-to-Text (`SpeechRecognition`), extract STT states, silence-detection timers, and event listeners into a reusable React hook `useSpeechToText` in [speech.ts](file:///home/pheem49/vscode/Project/Mint-CLI/src/renderer/shared/utils/speech.ts), and clean up both desktop (`src/renderer/src/components/ChatPanel.tsx`) and web (`src/renderer/src-web/components/ChatPanel.tsx`) `ChatPanel` components.
113
147
  - Implement 24-picture pagination, a manual "Load More" button, and API/image fetch cache-busters in `PicturesLibrary.tsx` and `tauri.ts` across desktop and web directories.
148
+ - Refactor file and folder icon mappings from `WorkspacePanel.tsx` into a central, reusable utility `src/renderer/shared/utils/fileIcons.ts`.
149
+ - Update `AgentActivityTable.tsx` to resolve target filenames/foldernames and display their respective Material Icon Theme SVGs instead of default wireframe outlines.
150
+ - Update Desktop and Web CSS stylesheets (`styles.css`) to support rendering and scaling Material Icon SVGs inside the agent activity tracker.
151
+ - Update `ChatPanel.tsx` (Desktop and Web) to strictly check the global `enableVoiceReply` setting. If voice reply is disabled but voice conversation mode is active, the microphone listening loop is automatically scheduled to resume without reading the AI response out loud.
114
152
 
115
153
  ### CLI Agent (`crates/mint-cli`)
116
154
  - Redefine live status print lines (`plan_lines`, `tasks_lines`, `activities_lines`, `explored_lines`) to accept progress tick state and apply the `get_bullet` helper.
@@ -123,6 +161,13 @@ Improved the autocomplete experience in the terminal:
123
161
  - Fixed `AgentProgress::Thinking` pattern matching compiler error (E0027) in `crates/mint-cli/src/agent.rs` and render active agent and model names in the live thinking terminal status bar.
124
162
  - Implement horizontal scrolling viewport for the CLI prompt input box in `crates/mint-cli/src/main.rs`, slicing the visible text to fit the terminal screen and adjusting the cursor column offset accordingly, allowing arbitrary length instructions.
125
163
  - Save `provider_change` system interaction events to the database and render a beautiful styled horizontal divider in the console when switching models via `/models` inside `crates/mint-cli/src/main.rs`.
164
+ - Implement dynamic skill invocation with `$` prefix in the CLI interactive chat.
165
+ - Add realtime autocomplete and pagination dropdown list for local/workspace and global skills under the `$` trigger, resolving and rendering skill descriptions.
166
+ - Add frontmatter YAML parser in `crates/mint-core/src/skills.rs` to fetch skill description values.
167
+ - Support confirmation dialogue to display skill contents and confirm activation before running the Agent, allowing inline or prompted tasks.
168
+ - Register `/skill add <path>` (and `/skill install <path>`) slash commands to automatically copy/install a skill file or folder into the global `~/.config/mint/mint-skills/` directory.
169
+
170
+
126
171
 
127
172
 
128
173
  ### Github CI/CD Workflows
@@ -189,39 +189,47 @@ pub async fn run_code_agent_with_options(
189
189
  agent_name,
190
190
  model_name,
191
191
  } => {
192
- if !options.fast_mode {
193
- if let Ok(mut status) = progress_live_status.lock() {
194
- let label = if let (Some(a), Some(m)) = (agent_name, model_name) {
195
- format!("{} ({}) is thinking ({} โ€ข Esc to interrupt)", a, m, format_elapsed(Duration::from_secs(elapsed_secs)))
196
- } else {
197
- format!("Thinking ({} โ€ข Esc to interrupt)", format_elapsed(Duration::from_secs(elapsed_secs)))
198
- };
199
- status.thinking = Some(label);
200
- render_live_status(&mut status);
201
- }
192
+ if !options.fast_mode
193
+ && let Ok(mut status) = progress_live_status.lock()
194
+ {
195
+ let label = if let (Some(a), Some(m)) = (agent_name, model_name) {
196
+ format!(
197
+ "{} ({}) is thinking ({} โ€ข Esc to interrupt)",
198
+ a,
199
+ m,
200
+ format_elapsed(Duration::from_secs(elapsed_secs))
201
+ )
202
+ } else {
203
+ format!(
204
+ "Thinking ({} โ€ข Esc to interrupt)",
205
+ format_elapsed(Duration::from_secs(elapsed_secs))
206
+ )
207
+ };
208
+ status.thinking = Some(label);
209
+ render_live_status(&mut status);
202
210
  }
203
211
  }
204
212
  AgentProgress::Thought { thought } => {
205
- if !options.fast_mode {
206
- if let Ok(mut status) = progress_live_status.lock() {
207
- commit_activity_snapshot(&mut status);
208
- print_timeline_note(&thought);
209
- status.thinking = None;
210
- render_live_status(&mut status);
211
- }
213
+ if !options.fast_mode
214
+ && let Ok(mut status) = progress_live_status.lock()
215
+ {
216
+ commit_activity_snapshot(&mut status);
217
+ print_timeline_note(&thought);
218
+ status.thinking = None;
219
+ render_live_status(&mut status);
212
220
  }
213
221
  }
214
222
  AgentProgress::ToolStart { action, input } => {
215
223
  if !options.fast_mode {
216
- if action == "create_plan" || action == "update_plan" {
217
- if let Some(steps) = extract_plan_steps(&input) {
218
- if let Ok(mut status) = progress_live_status.lock() {
219
- status.thinking = None;
220
- status.plan_steps = steps;
221
- render_live_status(&mut status);
222
- }
223
- return;
224
+ if (action == "create_plan" || action == "update_plan")
225
+ && let Some(steps) = extract_plan_steps(&input)
226
+ {
227
+ if let Ok(mut status) = progress_live_status.lock() {
228
+ status.thinking = None;
229
+ status.plan_steps = steps;
230
+ render_live_status(&mut status);
224
231
  }
232
+ return;
225
233
  }
226
234
  if let Some(label) = explored_action_label(&action, &input) {
227
235
  if let Ok(mut status) = progress_live_status.lock() {
@@ -313,12 +321,12 @@ pub async fn run_code_agent_with_options(
313
321
  } => {
314
322
  if !options.fast_mode {
315
323
  if action == "create_plan" || action == "update_plan" {
316
- if let Some(steps) = extract_plan_steps(&input) {
317
- if let Ok(mut status) = progress_live_status.lock() {
318
- status.thinking = None;
319
- status.plan_steps = steps;
320
- render_live_status(&mut status);
321
- }
324
+ if let Some(steps) = extract_plan_steps(&input)
325
+ && let Ok(mut status) = progress_live_status.lock()
326
+ {
327
+ status.thinking = None;
328
+ status.plan_steps = steps;
329
+ render_live_status(&mut status);
322
330
  }
323
331
  } else if command_was_run(&result)
324
332
  && let Some(commands) = ran_command_labels(&action, &input)
@@ -335,39 +343,38 @@ pub async fn run_code_agent_with_options(
335
343
  if action == "web_search"
336
344
  && !result.starts_with("Web search error:")
337
345
  && result != "No web search results found."
346
+ && let Ok(mut status) = progress_live_status.lock()
338
347
  {
339
- if let Ok(mut status) = progress_live_status.lock() {
340
- let sources = parse_web_search_sources(&result);
341
- status.web_sources.extend(sources);
342
- }
348
+ let sources = parse_web_search_sources(&result);
349
+ status.web_sources.extend(sources);
343
350
  }
344
351
  }
345
352
  };
346
353
 
347
354
  let chunk_live_status = Arc::clone(&live_status);
348
355
  let on_chunk = |summary: String| {
349
- if !options.fast_mode {
350
- if let Ok(mut status) = chunk_live_status.lock() {
351
- status.thinking = None;
352
- commit_activity_snapshot(&mut status);
353
- clear_live_status(&mut status);
354
- }
356
+ if !options.fast_mode
357
+ && let Ok(mut status) = chunk_live_status.lock()
358
+ {
359
+ status.thinking = None;
360
+ commit_activity_snapshot(&mut status);
361
+ clear_live_status(&mut status);
355
362
  }
356
363
  let formatted_summary = format_markdown_bold(&sanitize_latex(&summary));
357
364
  print!("\n {MINT}Mint:{RESET} ");
358
365
  render_live_summary(&formatted_summary);
359
366
 
360
367
  // Print web search sources if any were collected
361
- if let Ok(mut status) = chunk_live_status.lock() {
362
- if !status.web_sources.is_empty() {
363
- println!();
364
- println!(" {DIM}Sources:{RESET}");
365
- for (i, (title, url)) in status.web_sources.iter().enumerate() {
366
- println!(" {DIM}{}.{RESET} {BLUE}{}{RESET}", i + 1, title);
367
- println!(" {DIM}{}{RESET}", url);
368
- }
369
- status.web_sources.clear();
368
+ if let Ok(mut status) = chunk_live_status.lock()
369
+ && !status.web_sources.is_empty()
370
+ {
371
+ println!();
372
+ println!(" {DIM}Sources:{RESET}");
373
+ for (i, (title, url)) in status.web_sources.iter().enumerate() {
374
+ println!(" {DIM}{}.{RESET} {BLUE}{}{RESET}", i + 1, title);
375
+ println!(" {DIM}{}{RESET}", url);
370
376
  }
377
+ status.web_sources.clear();
371
378
  }
372
379
 
373
380
  println!();
@@ -396,14 +403,14 @@ pub async fn run_code_agent_with_options(
396
403
  }
397
404
  };
398
405
  agent_done.store(true, Ordering::Relaxed);
399
- if !options.fast_mode {
400
- if let Ok(mut status) = live_status.lock() {
401
- status.thinking = None;
402
- if res.is_err() {
403
- commit_activity_snapshot(&mut status);
404
- }
405
- clear_live_status(&mut status);
406
+ if !options.fast_mode
407
+ && let Ok(mut status) = live_status.lock()
408
+ {
409
+ status.thinking = None;
410
+ if res.is_err() {
411
+ commit_activity_snapshot(&mut status);
406
412
  }
413
+ clear_live_status(&mut status);
407
414
  }
408
415
  let res = res.map_err(|e| anyhow!("{}", e))?;
409
416
 
@@ -436,19 +443,19 @@ fn parse_web_search_sources(result: &str) -> Vec<(String, String)> {
436
443
  for line in result.lines() {
437
444
  let trimmed = line.trim();
438
445
  // Match numbered title lines: "1. Title text"
439
- if let Some(rest) = trimmed.split_once(". ") {
440
- if rest.0.parse::<usize>().is_ok() {
441
- current_title = Some(rest.1.trim().to_owned());
442
- continue;
443
- }
446
+ if let Some(rest) = trimmed.split_once(". ")
447
+ && rest.0.parse::<usize>().is_ok()
448
+ {
449
+ current_title = Some(rest.1.trim().to_owned());
450
+ continue;
444
451
  }
445
452
  // Match URL lines: "URL: https://..."
446
- if let Some(url) = trimmed.strip_prefix("URL: ") {
447
- if let Some(title) = current_title.take() {
448
- let url = url.trim().to_owned();
449
- if !url.is_empty() {
450
- sources.push((title, url));
451
- }
453
+ if let Some(url) = trimmed.strip_prefix("URL: ")
454
+ && let Some(title) = current_title.take()
455
+ {
456
+ let url = url.trim().to_owned();
457
+ if !url.is_empty() {
458
+ sources.push((title, url));
452
459
  }
453
460
  }
454
461
  }
@@ -680,7 +687,7 @@ fn render_live_status(status: &mut LiveStatus) {
680
687
  let stripped = strip_ansi_escapes(line);
681
688
  let line_len = stripped.chars().filter(|&c| !is_thai_combining(c)).count();
682
689
  let physical_lines = if width > 0 {
683
- (line_len + width - 1) / width
690
+ line_len.div_ceil(width)
684
691
  } else {
685
692
  1
686
693
  }
@@ -753,7 +760,11 @@ fn clear_live_status(status: &mut LiveStatus) {
753
760
 
754
761
  fn get_bullet(name: &str, is_thinking: bool, tick: usize) -> String {
755
762
  let char_str = if is_thinking {
756
- if (tick / 4) % 2 == 0 { "โ—" } else { "โ—‹" }
763
+ if (tick / 4).is_multiple_of(2) {
764
+ "โ—"
765
+ } else {
766
+ "โ—‹"
767
+ }
757
768
  } else {
758
769
  "โ—"
759
770
  };
@@ -966,7 +977,7 @@ fn format_markdown_bold(text: &str) -> String {
966
977
  } else {
967
978
  if c == '-' || c == '*' || c == '+' {
968
979
  let after = &line[idx + c.len_utf8()..];
969
- if after.chars().next().map_or(false, |c| c.is_whitespace()) {
980
+ if after.chars().next().is_some_and(|c| c.is_whitespace()) {
970
981
  is_list_item = true;
971
982
  marker_char = c;
972
983
  }
@@ -1043,27 +1054,27 @@ fn sanitize_latex(text: &str) -> String {
1043
1054
  let mut s = text.to_owned();
1044
1055
  for (pat, uni) in [
1045
1056
  // arrows
1046
- ("$\\rightarrow$", "โ†’"),
1047
- ("$\\leftarrow$", "โ†"),
1048
- ("$\\Rightarrow$", "โ‡’"),
1049
- ("$\\Leftarrow$", "โ‡"),
1057
+ ("$\\rightarrow$", "โ†’"),
1058
+ ("$\\leftarrow$", "โ†"),
1059
+ ("$\\Rightarrow$", "โ‡’"),
1060
+ ("$\\Leftarrow$", "โ‡"),
1050
1061
  ("$\\leftrightarrow$", "โ†”"),
1051
1062
  // comparison
1052
- ("$\\leq$", "โ‰ค"),
1053
- ("$\\geq$", "โ‰ฅ"),
1054
- ("$\\neq$", "โ‰ "),
1063
+ ("$\\leq$", "โ‰ค"),
1064
+ ("$\\geq$", "โ‰ฅ"),
1065
+ ("$\\neq$", "โ‰ "),
1055
1066
  ("$\\approx$", "โ‰ˆ"),
1056
1067
  // math
1057
1068
  ("$\\times$", "ร—"),
1058
- ("$\\div$", "รท"),
1059
- ("$\\pm$", "ยฑ"),
1069
+ ("$\\div$", "รท"),
1070
+ ("$\\pm$", "ยฑ"),
1060
1071
  ("$\\infty$", "โˆž"),
1061
- ("$\\cdot$", "ยท"),
1072
+ ("$\\cdot$", "ยท"),
1062
1073
  // sets
1063
- ("$\\in$", "โˆˆ"),
1074
+ ("$\\in$", "โˆˆ"),
1064
1075
  ("$\\subset$", "โŠ‚"),
1065
- ("$\\cup$", "โˆช"),
1066
- ("$\\cap$", "โˆฉ"),
1076
+ ("$\\cup$", "โˆช"),
1077
+ ("$\\cap$", "โˆฉ"),
1067
1078
  ] {
1068
1079
  s = s.replace(pat, uni);
1069
1080
  }
@@ -27,21 +27,21 @@ pub fn read_clipboard_image() -> Result<Option<String>> {
27
27
  if let Ok(output) = std::process::Command::new("xclip")
28
28
  .args(["-selection", "clipboard", "-t", "image/png", "-o"])
29
29
  .output()
30
+ && output.status.success()
31
+ && !output.stdout.is_empty()
30
32
  {
31
- if output.status.success() && !output.stdout.is_empty() {
32
- let encoded = BASE64.encode(&output.stdout);
33
- return Ok(Some(format!("data:image/png;base64,{encoded}")));
34
- }
33
+ let encoded = BASE64.encode(&output.stdout);
34
+ return Ok(Some(format!("data:image/png;base64,{encoded}")));
35
35
  }
36
36
 
37
37
  if let Ok(output) = std::process::Command::new("wl-paste")
38
38
  .args(["--type", "image/png"])
39
39
  .output()
40
+ && output.status.success()
41
+ && !output.stdout.is_empty()
40
42
  {
41
- if output.status.success() && !output.stdout.is_empty() {
42
- let encoded = BASE64.encode(&output.stdout);
43
- return Ok(Some(format!("data:image/png;base64,{encoded}")));
44
- }
43
+ let encoded = BASE64.encode(&output.stdout);
44
+ return Ok(Some(format!("data:image/png;base64,{encoded}")));
45
45
  }
46
46
 
47
47
  Ok(None)