| ... | ... | @@ -1,5 +1,5 @@ |
| 1 | 1 | //! Linux, on X11 and Wayland: window controls drawn in the title bar, files chosen and opened |
| 2 | | //! through the XDG desktop portal, other dialogs through zenity, kdialog or Snowbound's own, |
| 2 | //! through the XDG desktop portal, other dialogs drawn in the window, |
| 3 | 3 | //! the colour scheme from the settings portal, and text conventions from the C library's |
| 4 | 4 | //! locale. |
| 5 | 5 | |
| ... | ... | @@ -9,7 +9,7 @@ use std::{ |
| 9 | 9 | cell::UnsafeCell, |
| 10 | 10 | collections::HashMap, |
| 11 | 11 | error::Error, |
| 12 | | ffi::{CStr, CString, OsStr, OsString, c_char, c_void}, |
| 12 | ffi::{CStr, CString, OsString, c_char, c_void}, |
| 13 | 13 | os::unix::ffi::OsStringExt, |
| 14 | 14 | path::PathBuf, |
| 15 | 15 | process::Command, |
| ... | ... | @@ -1390,59 +1390,21 @@ pub fn date_text(filetime: u64) -> [String; 2] { |
| 1390 | 1390 | date_labels(&local_time(filetime)) |
| 1391 | 1391 | } |
| 1392 | 1392 | |
| 1393 | | /// Asks for the page's date or time with the desktop's dialog tool, or else as text in |
| 1394 | | /// Snowbound's own dialog. |
| 1393 | /// Asks for the page's date or time as text. |
| 1395 | 1394 | pub fn edit_date( |
| 1396 | 1395 | timestamp: u64, |
| 1397 | 1396 | field: DateField, |
| 1398 | 1397 | title: &str, |
| 1399 | 1398 | reply: Reply<Result<(u64, [String; 2]), &'static str>>, |
| 1400 | 1399 | ) { |
| 1401 | | let title = title.to_owned(); |
| 1400 | let tm = local_time(timestamp); |
| 1401 | let (detail, text) = match field { |
| 1402 | DateField::Date => ("Date, as year-month-day:", format(&tm, c"%Y-%m-%d")), |
| 1403 | DateField::Time => ("Time, as hours and minutes:", format(&tm, c"%H:%M")), |
| 1404 | }; |
| 1402 | 1405 | let reply = reply |
| 1403 | 1406 | .map(move |answer: String| changed_date(timestamp, local_time(timestamp), field, &answer)); |
| 1404 | | std::thread::spawn(move || { |
| 1405 | | let tm = local_time(timestamp); |
| 1406 | | let (asked, detail, value) = match field { |
| 1407 | | DateField::Date => ( |
| 1408 | | dialog( |
| 1409 | | &[ |
| 1410 | | "--calendar", |
| 1411 | | &format!("--title={title}"), |
| 1412 | | "--text=", |
| 1413 | | &format!("--day={}", tm.tm_mday), |
| 1414 | | &format!("--month={}", tm.tm_mon + 1), |
| 1415 | | &format!("--year={}", tm.tm_year + 1900), |
| 1416 | | "--date-format=%Y-%m-%d", |
| 1417 | | ], |
| 1418 | | &["--calendar", &title, "--dateformat", "yyyy-MM-dd"], |
| 1419 | | ), |
| 1420 | | "Date, as year-month-day:", |
| 1421 | | format(&tm, c"%Y-%m-%d"), |
| 1422 | | ), |
| 1423 | | DateField::Time => { |
| 1424 | | let current = format(&tm, c"%H:%M"); |
| 1425 | | let asked = dialog( |
| 1426 | | &[ |
| 1427 | | "--entry", |
| 1428 | | &format!("--title={title}"), |
| 1429 | | "--text=Time, as hours and minutes:", |
| 1430 | | &format!("--entry-text={current}"), |
| 1431 | | ], |
| 1432 | | &["--inputbox", "Time, as hours and minutes:", &current], |
| 1433 | | ); |
| 1434 | | (asked, "Time, as hours and minutes:", current) |
| 1435 | | } |
| 1436 | | }; |
| 1437 | | match asked { |
| 1438 | | Ok(Some(answer)) => reply.send(answer), |
| 1439 | | Ok(None) => {} |
| 1440 | | Err(_) => { |
| 1441 | | let ask = Ask::Entry { text: value, reply }; |
| 1442 | | crate::dialog::show(title, detail.to_owned(), ask); |
| 1443 | | } |
| 1444 | | } |
| 1445 | | }); |
| 1407 | crate::dialog::show(title.into(), detail.into(), Ask::Entry { text, reply }); |
| 1446 | 1408 | } |
| 1447 | 1409 | |
| 1448 | 1410 | /// `timestamp` with `field` changed to what the user typed or chose, `answer`. |
| ... | ... | @@ -1499,22 +1461,6 @@ fn merge_date( |
| 1499 | 1461 | Ok((updated, date_labels(&tm))) |
| 1500 | 1462 | } |
| 1501 | 1463 | |
| 1502 | | /// Runs zenity, or kdialog where zenity is missing, waiting for the answer: None when |
| 1503 | | /// cancelled, and an error when neither is installed. |
| 1504 | | fn dialog( |
| 1505 | | zenity: &[impl AsRef<OsStr>], |
| 1506 | | kdialog: &[impl AsRef<OsStr>], |
| 1507 | | ) -> std::io::Result<Option<String>> { |
| 1508 | | let output = Command::new("zenity") |
| 1509 | | .args(zenity) |
| 1510 | | .output() |
| 1511 | | .or_else(|_| Command::new("kdialog").args(kdialog).output())?; |
| 1512 | | Ok(output |
| 1513 | | .status |
| 1514 | | .success() |
| 1515 | | .then(|| String::from_utf8_lossy(&output.stdout).trim().to_owned())) |
| 1516 | | } |
| 1517 | | |
| 1518 | 1464 | /// Asks for a file to insert, one of `types` (extensions) unless empty, titled `title`. |
| 1519 | 1465 | pub fn pick_file(title: &str, types: &[&str], reply: Reply<PathBuf>) { |
| 1520 | 1466 | let globs: Vec<_> = types.iter().map(|kind| format!("*.{kind}")).collect(); |
| ... | ... | @@ -1531,12 +1477,6 @@ pub fn pick_file(title: &str, types: &[&str], reply: Reply<PathBuf>) { |
| 1531 | 1477 | title, |
| 1532 | 1478 | "OpenFile", |
| 1533 | 1479 | options, |
| 1534 | | &[ |
| 1535 | | "--file-selection", |
| 1536 | | &format!("--title={title}"), |
| 1537 | | &format!("--file-filter={patterns}"), |
| 1538 | | ], |
| 1539 | | &["--getopenfilename", ".", &patterns, "--title", title], |
| 1540 | 1480 | move |reply| Ask::File { |
| 1541 | 1481 | folder: crate::dialog::start_folder(), |
| 1542 | 1482 | types, |
| ... | ... | @@ -1554,21 +1494,16 @@ fn filters(name: &str, globs: &[String]) -> Value<'static> { |
| 1554 | 1494 | Value::from(vec![(name.to_owned(), globs)]) |
| 1555 | 1495 | } |
| 1556 | 1496 | |
| 1557 | | /// Replies with the file the file chooser portal answers `method` with, titled `title`; |
| 1558 | | /// where no portal answers, zenity's or kdialog's, given their arguments; and where neither |
| 1559 | | /// is installed, Snowbound's `own`. |
| 1497 | /// Replies with the file the file chooser portal answers `method` with, titled `title`, or |
| 1498 | /// where no portal answers, Snowbound's `own`. |
| 1560 | 1499 | fn choose( |
| 1561 | 1500 | title: &str, |
| 1562 | 1501 | method: &'static str, |
| 1563 | 1502 | mut options: HashMap<&'static str, Value<'static>>, |
| 1564 | | zenity: &[&str], |
| 1565 | | kdialog: &[&str], |
| 1566 | 1503 | own: impl FnOnce(Reply<PathBuf>) -> Ask + Send + 'static, |
| 1567 | 1504 | reply: Reply<PathBuf>, |
| 1568 | 1505 | ) { |
| 1569 | 1506 | let title = title.to_owned(); |
| 1570 | | let [zenity, kdialog]: [Vec<String>; 2] = |
| 1571 | | [zenity, kdialog].map(|args| args.iter().map(|arg| arg.to_string()).collect()); |
| 1572 | 1507 | std::thread::spawn(move || { |
| 1573 | 1508 | let chosen = portal("org.freedesktop.portal.FileChooser") |
| 1574 | 1509 | .map_err(Into::into) |
| ... | ... | @@ -1578,20 +1513,18 @@ fn choose( |
| 1578 | 1513 | chooser.call(method, &("", &title, &options)) |
| 1579 | 1514 | }) |
| 1580 | 1515 | }); |
| 1581 | | let path = match chosen { |
| 1582 | | Ok(results) => results.and_then(|results| { |
| 1583 | | let uris: Vec<String> = results.get("uris")?.try_clone().ok()?.try_into().ok()?; |
| 1584 | | let path = uris.first()?.strip_prefix("file://")?; |
| 1585 | | Some(OsString::from_vec(crate::paste::percent_decode(path)).into()) |
| 1586 | | }), |
| 1516 | let results = match chosen { |
| 1517 | Ok(results) => results, |
| 1587 | 1518 | Err(error) => { |
| 1588 | 1519 | eprintln!("The file chooser portal failed: {error}"); |
| 1589 | | match dialog(&zenity, &kdialog) { |
| 1590 | | Ok(path) => path.filter(|path| !path.is_empty()).map(PathBuf::from), |
| 1591 | | Err(_) => return crate::dialog::show(title, String::new(), own(reply)), |
| 1592 | | } |
| 1520 | return crate::dialog::show(title, String::new(), own(reply)); |
| 1593 | 1521 | } |
| 1594 | 1522 | }; |
| 1523 | let path = results.and_then(|results| { |
| 1524 | let uris: Vec<String> = results.get("uris")?.try_clone().ok()?.try_into().ok()?; |
| 1525 | let path = uris.first()?.strip_prefix("file://")?; |
| 1526 | Some(OsString::from_vec(crate::paste::percent_decode(path)).into()) |
| 1527 | }); |
| 1595 | 1528 | if let Some(path) = path { |
| 1596 | 1529 | reply.send(path); |
| 1597 | 1530 | } |
| ... | ... | @@ -1692,39 +1625,14 @@ pub fn reveal(target: impl AsRef<std::ffi::OsStr>) { |
| 1692 | 1625 | } |
| 1693 | 1626 | } |
| 1694 | 1627 | |
| 1695 | | /// Asks whether to go ahead with `action`, through zenity or kdialog where installed, or |
| 1696 | | /// else Snowbound's own dialog. |
| 1628 | /// Asks whether to go ahead with `action`, offering `cancel` first. |
| 1697 | 1629 | pub fn confirm(message: &str, detail: &str, cancel: &str, action: &str, reply: Reply<()>) { |
| 1698 | | let [message, detail, cancel, action] = [message, detail, cancel, action].map(str::to_owned); |
| 1699 | | std::thread::spawn(move || { |
| 1700 | | let status = Command::new("zenity") |
| 1701 | | .args([ |
| 1702 | | "--question", |
| 1703 | | &format!("--title={message}"), |
| 1704 | | &format!("--text={detail}"), |
| 1705 | | &format!("--ok-label={action}"), |
| 1706 | | &format!("--cancel-label={cancel}"), |
| 1707 | | ]) |
| 1708 | | .status() |
| 1709 | | .or_else(|_| { |
| 1710 | | Command::new("kdialog") |
| 1711 | | .args(["--warningcontinuecancel", &detail, "--title", &message]) |
| 1712 | | .args(["--continue-label", &action]) |
| 1713 | | .status() |
| 1714 | | }); |
| 1715 | | match status { |
| 1716 | | Ok(status) if status.success() => reply.send(()), |
| 1717 | | Ok(_) => {} |
| 1718 | | Err(_) => { |
| 1719 | | let ask = Ask::Question { |
| 1720 | | cancel, |
| 1721 | | action, |
| 1722 | | reply, |
| 1723 | | }; |
| 1724 | | crate::dialog::show(message, detail, ask); |
| 1725 | | } |
| 1726 | | } |
| 1727 | | }); |
| 1630 | let ask = Ask::Question { |
| 1631 | cancel: cancel.into(), |
| 1632 | action: action.into(), |
| 1633 | reply, |
| 1634 | }; |
| 1635 | crate::dialog::show(message.into(), detail.into(), ask); |
| 1728 | 1636 | } |
| 1729 | 1637 | |
| 1730 | 1638 | /// Asks for a notebook's table of contents or a section file, titled `title`. |
| ... | ... | @@ -1736,12 +1644,6 @@ pub fn pick_notebook(title: &str, reply: Reply<PathBuf>) { |
| 1736 | 1644 | title, |
| 1737 | 1645 | "OpenFile", |
| 1738 | 1646 | HashMap::from([("filters", filters(name, &globs))]), |
| 1739 | | &[ |
| 1740 | | "--file-selection", |
| 1741 | | &format!("--title={title}"), |
| 1742 | | &format!("--file-filter={name} | {}", globs.join(" ")), |
| 1743 | | ], |
| 1744 | | &["--getopenfilename", ".", &globs.join(" "), "--title", title], |
| 1745 | 1647 | move |reply| Ask::File { |
| 1746 | 1648 | folder: crate::dialog::start_folder(), |
| 1747 | 1649 | types: types.map(String::from).to_vec(), |
| ... | ... | @@ -1780,13 +1682,6 @@ pub fn pick_new( |
| 1780 | 1682 | title, |
| 1781 | 1683 | "SaveFile", |
| 1782 | 1684 | options, |
| 1783 | | &[ |
| 1784 | | "--file-selection", |
| 1785 | | "--save", |
| 1786 | | &format!("--title={title}"), |
| 1787 | | &format!("--filename={name}"), |
| 1788 | | ], |
| 1789 | | &["--getsavefilename", name, "--title", title], |
| 1790 | 1685 | move |reply| Ask::File { |
| 1791 | 1686 | folder, |
| 1792 | 1687 | types: Vec::new(), |
| ... | ... | @@ -1800,35 +1695,12 @@ pub fn pick_new( |
| 1800 | 1695 | |
| 1801 | 1696 | /// Tells the user something they asked for could not be done: `message`, then what to do. |
| 1802 | 1697 | pub fn alert(message: &str, detail: &str) { |
| 1803 | | show(["--warning", "--sorry"], message, detail); |
| 1698 | crate::dialog::show(message.into(), detail.into(), Ask::Message); |
| 1804 | 1699 | } |
| 1805 | 1700 | |
| 1806 | 1701 | /// Tells the user how something they asked for turned out. |
| 1807 | 1702 | pub fn inform(message: &str, detail: &str) { |
| 1808 | | show(["--info", "--msgbox"], message, detail); |
| 1809 | | } |
| 1810 | | |
| 1811 | | /// Shows `message` and `detail` in zenity's or else kdialog's dialog of the `kinds`, or else |
| 1812 | | /// in Snowbound's own. |
| 1813 | | fn show([zenity, kdialog]: [&'static str; 2], message: &str, detail: &str) { |
| 1814 | | let (title, text) = (message.to_owned(), detail.to_owned()); |
| 1815 | | std::thread::spawn(move || { |
| 1816 | | let shown = Command::new("zenity") |
| 1817 | | .args([ |
| 1818 | | zenity, |
| 1819 | | &format!("--title={title}"), |
| 1820 | | &format!("--text={text}"), |
| 1821 | | ]) |
| 1822 | | .status() |
| 1823 | | .or_else(|_| { |
| 1824 | | Command::new("kdialog") |
| 1825 | | .args([kdialog, &text, "--title", &title]) |
| 1826 | | .status() |
| 1827 | | }); |
| 1828 | | if shown.is_err() { |
| 1829 | | crate::dialog::show(title, text, Ask::Message); |
| 1830 | | } |
| 1831 | | }); |
| 1703 | alert(message, detail); |
| 1832 | 1704 | } |
| 1833 | 1705 | |
| 1834 | 1706 | #[cfg(test)] |