| ... | @@ -1,5 +1,5 @@ | ... | @@ -1,5 +1,5 @@ |
| 1 | //! Linux, on X11 and Wayland: window controls drawn in the title bar, files chosen and opened | 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 | //! the colour scheme from the settings portal, and text conventions from the C library's | 3 | //! the colour scheme from the settings portal, and text conventions from the C library's |
| 4 | //! locale. | 4 | //! locale. |
| 5 | | 5 | |
| ... | @@ -9,7 +9,7 @@ use std::{ | ... | @@ -9,7 +9,7 @@ use std::{ |
| 9 | cell::UnsafeCell, | 9 | cell::UnsafeCell, |
| 10 | collections::HashMap, | 10 | collections::HashMap, |
| 11 | error::Error, | 11 | error::Error, |
| 12 | ffi::{CStr, CString, OsStr, OsString, c_char, c_void}, | 12 | ffi::{CStr, CString, OsString, c_char, c_void}, |
| 13 | os::unix::ffi::OsStringExt, | 13 | os::unix::ffi::OsStringExt, |
| 14 | path::PathBuf, | 14 | path::PathBuf, |
| 15 | process::Command, | 15 | process::Command, |
| ... | @@ -1390,59 +1390,21 @@ pub fn date_text(filetime: u64) -> [String; 2] { | ... | @@ -1390,59 +1390,21 @@ pub fn date_text(filetime: u64) -> [String; 2] { |
| 1390 | date_labels(&local_time(filetime)) | 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 | 1393 | /// Asks for the page's date or time as text. |
| 1394 | /// Snowbound's own dialog. | | |
| 1395 | pub fn edit_date( | 1394 | pub fn edit_date( |
| 1396 | timestamp: u64, | 1395 | timestamp: u64, |
| 1397 | field: DateField, | 1396 | field: DateField, |
| 1398 | title: &str, | 1397 | title: &str, |
| 1399 | reply: Reply<Result<(u64, [String; 2]), &'static str>>, | 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 | let reply = reply | 1405 | let reply = reply |
| 1403 | .map(move |answer: String| changed_date(timestamp, local_time(timestamp), field, &answer)); | 1406 | .map(move |answer: String| changed_date(timestamp, local_time(timestamp), field, &answer)); |
| 1404 | std::thread::spawn(move || { | 1407 | crate::dialog::show(title.into(), detail.into(), Ask::Entry { text, reply }); |
| 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 | }); | | |
| 1446 | } | 1408 | } |
| 1447 | | 1409 | |
| 1448 | /// `timestamp` with `field` changed to what the user typed or chose, `answer`. | 1410 | /// `timestamp` with `field` changed to what the user typed or chose, `answer`. |
| ... | @@ -1499,22 +1461,6 @@ fn merge_date( | ... | @@ -1499,22 +1461,6 @@ fn merge_date( |
| 1499 | Ok((updated, date_labels(&tm))) | 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 | /// Asks for a file to insert, one of `types` (extensions) unless empty, titled `title`. | 1464 | /// Asks for a file to insert, one of `types` (extensions) unless empty, titled `title`. |
| 1519 | pub fn pick_file(title: &str, types: &[&str], reply: Reply<PathBuf>) { | 1465 | pub fn pick_file(title: &str, types: &[&str], reply: Reply<PathBuf>) { |
| 1520 | let globs: Vec<_> = types.iter().map(|kind| format!("*.{kind}")).collect(); | 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,12 +1477,6 @@ pub fn pick_file(title: &str, types: &[&str], reply: Reply<PathBuf>) { |
| 1531 | title, | 1477 | title, |
| 1532 | "OpenFile", | 1478 | "OpenFile", |
| 1533 | options, | 1479 | options, |
| 1534 | &[ | | |
| 1535 | "--file-selection", | | |
| 1536 | &format!("--title={title}"), | | |
| 1537 | &format!("--file-filter={patterns}"), | | |
| 1538 | ], | | |
| 1539 | &["--getopenfilename", ".", &patterns, "--title", title], | | |
| 1540 | move |reply| Ask::File { | 1480 | move |reply| Ask::File { |
| 1541 | folder: crate::dialog::start_folder(), | 1481 | folder: crate::dialog::start_folder(), |
| 1542 | types, | 1482 | types, |
| ... | @@ -1554,21 +1494,16 @@ fn filters(name: &str, globs: &[String]) -> Value<'static> { | ... | @@ -1554,21 +1494,16 @@ fn filters(name: &str, globs: &[String]) -> Value<'static> { |
| 1554 | Value::from(vec![(name.to_owned(), globs)]) | 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`; | 1497 | /// Replies with the file the file chooser portal answers `method` with, titled `title`, or |
| 1558 | /// where no portal answers, zenity's or kdialog's, given their arguments; and where neither | 1498 | /// where no portal answers, Snowbound's `own`. |
| 1559 | /// is installed, Snowbound's `own`. | | |
| 1560 | fn choose( | 1499 | fn choose( |
| 1561 | title: &str, | 1500 | title: &str, |
| 1562 | method: &'static str, | 1501 | method: &'static str, |
| 1563 | mut options: HashMap<&'static str, Value<'static>>, | 1502 | mut options: HashMap<&'static str, Value<'static>>, |
| 1564 | zenity: &[&str], | | |
| 1565 | kdialog: &[&str], | | |
| 1566 | own: impl FnOnce(Reply<PathBuf>) -> Ask + Send + 'static, | 1503 | own: impl FnOnce(Reply<PathBuf>) -> Ask + Send + 'static, |
| 1567 | reply: Reply<PathBuf>, | 1504 | reply: Reply<PathBuf>, |
| 1568 | ) { | 1505 | ) { |
| 1569 | let title = title.to_owned(); | 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 | std::thread::spawn(move || { | 1507 | std::thread::spawn(move || { |
| 1573 | let chosen = portal("org.freedesktop.portal.FileChooser") | 1508 | let chosen = portal("org.freedesktop.portal.FileChooser") |
| 1574 | .map_err(Into::into) | 1509 | .map_err(Into::into) |
| ... | @@ -1578,20 +1513,18 @@ fn choose( | ... | @@ -1578,20 +1513,18 @@ fn choose( |
| 1578 | chooser.call(method, &("", &title, &options)) | 1513 | chooser.call(method, &("", &title, &options)) |
| 1579 | }) | 1514 | }) |
| 1580 | }); | 1515 | }); |
| 1581 | let path = match chosen { | 1516 | let results = match chosen { |
| 1582 | Ok(results) => results.and_then(|results| { | 1517 | Ok(results) => 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 | }), | | |
| 1587 | Err(error) => { | 1518 | Err(error) => { |
| 1588 | eprintln!("The file chooser portal failed: {error}"); | 1519 | eprintln!("The file chooser portal failed: {error}"); |
| 1589 | match dialog(&zenity, &kdialog) { | 1520 | return crate::dialog::show(title, String::new(), own(reply)); |
| 1590 | Ok(path) => path.filter(|path| !path.is_empty()).map(PathBuf::from), | | |
| 1591 | Err(_) => return crate::dialog::show(title, String::new(), own(reply)), | | |
| 1592 | } | | |
| 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 | if let Some(path) = path { | 1528 | if let Some(path) = path { |
| 1596 | reply.send(path); | 1529 | reply.send(path); |
| 1597 | } | 1530 | } |
| ... | @@ -1692,39 +1625,14 @@ pub fn reveal(target: impl AsRef<std::ffi::OsStr>) { | ... | @@ -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 | 1628 | /// Asks whether to go ahead with `action`, offering `cancel` first. |
| 1696 | /// else Snowbound's own dialog. | | |
| 1697 | pub fn confirm(message: &str, detail: &str, cancel: &str, action: &str, reply: Reply<()>) { | 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); | 1630 | let ask = Ask::Question { |
| 1699 | std::thread::spawn(move || { | 1631 | cancel: cancel.into(), |
| 1700 | let status = Command::new("zenity") | 1632 | action: action.into(), |
| 1701 | .args([ | 1633 | reply, |
| 1702 | "--question", | 1634 | }; |
| 1703 | &format!("--title={message}"), | 1635 | crate::dialog::show(message.into(), detail.into(), ask); |
| 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 | }); | | |
| 1728 | } | 1636 | } |
| 1729 | | 1637 | |
| 1730 | /// Asks for a notebook's table of contents or a section file, titled `title`. | 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,12 +1644,6 @@ pub fn pick_notebook(title: &str, reply: Reply<PathBuf>) { |
| 1736 | title, | 1644 | title, |
| 1737 | "OpenFile", | 1645 | "OpenFile", |
| 1738 | HashMap::from([("filters", filters(name, &globs))]), | 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 | move |reply| Ask::File { | 1647 | move |reply| Ask::File { |
| 1746 | folder: crate::dialog::start_folder(), | 1648 | folder: crate::dialog::start_folder(), |
| 1747 | types: types.map(String::from).to_vec(), | 1649 | types: types.map(String::from).to_vec(), |
| ... | @@ -1780,13 +1682,6 @@ pub fn pick_new( | ... | @@ -1780,13 +1682,6 @@ pub fn pick_new( |
| 1780 | title, | 1682 | title, |
| 1781 | "SaveFile", | 1683 | "SaveFile", |
| 1782 | options, | 1684 | options, |
| 1783 | &[ | | |
| 1784 | "--file-selection", | | |
| 1785 | "--save", | | |
| 1786 | &format!("--title={title}"), | | |
| 1787 | &format!("--filename={name}"), | | |
| 1788 | ], | | |
| 1789 | &["--getsavefilename", name, "--title", title], | | |
| 1790 | move |reply| Ask::File { | 1685 | move |reply| Ask::File { |
| 1791 | folder, | 1686 | folder, |
| 1792 | types: Vec::new(), | 1687 | types: Vec::new(), |
| ... | @@ -1800,35 +1695,12 @@ pub fn pick_new( | ... | @@ -1800,35 +1695,12 @@ pub fn pick_new( |
| 1800 | | 1695 | |
| 1801 | /// Tells the user something they asked for could not be done: `message`, then what to do. | 1696 | /// Tells the user something they asked for could not be done: `message`, then what to do. |
| 1802 | pub fn alert(message: &str, detail: &str) { | 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 | /// Tells the user how something they asked for turned out. | 1701 | /// Tells the user how something they asked for turned out. |
| 1807 | pub fn inform(message: &str, detail: &str) { | 1702 | pub fn inform(message: &str, detail: &str) { |
| 1808 | show(["--info", "--msgbox"], message, detail); | 1703 | alert(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 | }); | | |
| 1832 | } | 1704 | } |
| 1833 | | 1705 | |
| 1834 | #[cfg(test)] | 1706 | #[cfg(test)] |