about summary refs log tree commit diff
diff options
context:
space:
mode:
authorFelix Zwettler <f.zwettler@posteo.de>2022-06-07 23:14:29 +0200
committerFelix Zwettler <f.zwettler@posteo.de>2022-06-07 23:34:00 +0200
commita510603825050a5face70d5387f0be830a4d685b (patch)
treeaac28a23249c1d483da25304a9e75c925bda68f5
parent67679138574b083d838294bf82b6058a54b6a13d (diff)
downloadFurtherance-a510603825050a5face70d5387f0be830a4d685b.tar.zst
add csv export
-rwxr-xr-x.gitignore3
-rwxr-xr-xCargo.lock82
-rwxr-xr-xCargo.toml5
-rwxr-xr-xsrc/application.rs7
-rwxr-xr-xsrc/database.rs178
-rwxr-xr-xsrc/furtherance.gresource.xml1
-rw-r--r--src/gtk/dialogs.ui100
-rwxr-xr-xsrc/gtk/window.ui4
-rw-r--r--src/ui/report.rs140
-rwxr-xr-xsrc/ui/tasks_page.rs16
-rwxr-xr-xsrc/ui/window.rs230
11 files changed, 615 insertions, 151 deletions
diff --git a/.gitignore b/.gitignore
index 1cea60c..b91e3fd 100755
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,5 @@
 com.lakoliu.Furtherance.json~
 src/config.rs
+/.flatpak
+/.vscode
+/target
\ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
index 7e20984..0ee3f73 100755
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -67,6 +67,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
 
 [[package]]
+name = "bstr"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
+dependencies = [
+ "lazy_static",
+ "memchr",
+ "regex-automata",
+ "serde",
+]
+
+[[package]]
 name = "cairo-rs"
 version = "0.15.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -140,6 +152,28 @@ dependencies = [
 ]
 
 [[package]]
+name = "csv"
+version = "1.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1"
+dependencies = [
+ "bstr",
+ "csv-core",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "csv-core"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
 name = "dbus"
 version = "0.9.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -213,7 +247,9 @@ dependencies = [
 name = "furtherance"
 version = "1.4.0"
 dependencies = [
+ "anyhow",
  "chrono",
+ "csv",
  "dbus",
  "dbus-codegen",
  "directories",
@@ -223,8 +259,11 @@ dependencies = [
  "itertools",
  "libadwaita",
  "log",
+ "num-derive",
+ "num-traits",
  "once_cell",
  "rusqlite",
+ "serde",
 ]
 
 [[package]]
@@ -609,6 +648,12 @@ dependencies = [
 ]
 
 [[package]]
+name = "itoa"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
+
+[[package]]
 name = "lazy_static"
 version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -718,6 +763,17 @@ dependencies = [
 ]
 
 [[package]]
+name = "num-derive"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
 name = "num-integer"
 version = "0.1.44"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -915,6 +971,12 @@ dependencies = [
 ]
 
 [[package]]
+name = "regex-automata"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
+
+[[package]]
 name = "regex-syntax"
 version = "0.6.25"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -945,6 +1007,12 @@ dependencies = [
 ]
 
 [[package]]
+name = "ryu"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695"
+
+[[package]]
 name = "semver"
 version = "0.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -967,6 +1035,20 @@ name = "serde"
 version = "1.0.136"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.136"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
 
 [[package]]
 name = "slab"
diff --git a/Cargo.toml b/Cargo.toml
index 97078f2..a870325 100755
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -15,6 +15,11 @@ dbus-codegen = "0.10.0"
 log = "0.4"
 gtk4-macros = "0.4.8"
 itertools = "0.10.3"
+anyhow = "1.0"
+serde = { version = "1.0", features = ["derive"] }
+csv = "1.1.6"
+num-traits = "0.2.14"
+num-derive = "0.3.3"
 
 [dependencies.gtk]
 package = "gtk4"
diff --git a/src/application.rs b/src/application.rs
index ad6ee51..fdc3637 100755
--- a/src/application.rs
+++ b/src/application.rs
@@ -156,6 +156,13 @@ impl FurtheranceApplication {
             imp.pomodoro_dialog.lock().unwrap().response(gtk::ResponseType::Reject);
         }));
         self.add_action(&stop_pomodoro_action);
+
+        let export_csv_action = gio::SimpleAction::new("export-csv", None);
+        export_csv_action.connect_activate(clone!(@weak self as app => move |_, _| {
+            let window = FurtheranceWindow::default();
+            window.open_csv_export_dialog();
+        }));
+        self.add_action(&export_csv_action);
     }
 
     fn setup_application(&self) {
diff --git a/src/database.rs b/src/database.rs
index a2485c6..0ccf693 100755
--- a/src/database.rs
+++ b/src/database.rs
@@ -14,13 +14,15 @@
 // You should have received a copy of the GNU General Public License
 // along with this program.  If not, see <https://www.gnu.org/licenses/>.
 
-use rusqlite::{Connection, Result};
 use chrono::{DateTime, Local};
 use directories::ProjectDirs;
-use std::path::PathBuf;
+use gtk::glib;
+use rusqlite::{Connection, Result};
+use std::convert::TryFrom;
 use std::fs::create_dir_all;
+use std::path::PathBuf;
 
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
 pub struct Task {
     pub id: i32,
     pub task_name: String,
@@ -29,12 +31,104 @@ pub struct Task {
     pub tags: String,
 }
 
+#[derive(
+    Debug,
+    Clone,
+    Copy,
+    PartialEq,
+    Eq,
+    PartialOrd,
+    Ord,
+    num_derive::FromPrimitive,
+    num_derive::ToPrimitive,
+    glib::Enum,
+)]
+#[enum_type(name = "SortOrder")]
+pub enum SortOrder {
+    #[enum_value(name = "Ascending", nick = "ascending")]
+    Ascending = 0,
+    #[enum_value(name = "Descending", nick = "descending")]
+    Descending,
+}
+
+impl Default for SortOrder {
+    fn default() -> Self {
+        // matches the default in sqlite
+        Self::Ascending
+    }
+}
+
+impl TryFrom<u32> for SortOrder {
+    type Error = anyhow::Error;
+
+    fn try_from(value: u32) -> Result<Self, Self::Error> {
+        num_traits::FromPrimitive::from_u32(value)
+            .ok_or_else(|| anyhow::anyhow!("SortOrder from_u32() failed for value {}", value))
+    }
+}
+
+impl SortOrder {
+    fn to_sqlite(&self) -> &str {
+        match self {
+            SortOrder::Ascending => "ASC",
+            SortOrder::Descending => "DESC",
+        }
+    }
+}
+
+#[derive(
+    Debug,
+    Clone,
+    Copy,
+    PartialEq,
+    Eq,
+    PartialOrd,
+    Ord,
+    num_derive::ToPrimitive,
+    num_derive::FromPrimitive,
+    glib::Enum,
+)]
+#[enum_type(name = "TaskSort")]
+pub enum TaskSort {
+    #[enum_value(name = "StartTime", nick = "start time")]
+    StartTime,
+    #[enum_value(name = "StopTime", nick = "stop time")]
+    StopTime,
+    #[enum_value(name = "TaskName", nick = "task name")]
+    TaskName,
+}
+
+impl Default for TaskSort {
+    fn default() -> Self {
+        Self::StartTime
+    }
+}
+
+impl TryFrom<u32> for TaskSort {
+    type Error = anyhow::Error;
+
+    fn try_from(value: u32) -> Result<Self, Self::Error> {
+        num_traits::FromPrimitive::from_u32(value)
+            .ok_or_else(|| anyhow::anyhow!("TaskSort from_u32() failed for value {}", value))
+    }
+}
+
+impl TaskSort {
+    fn to_sqlite(&self) -> &str {
+        match self {
+            Self::StartTime => "start_time",
+            Self::StopTime => "stop_time",
+            Self::TaskName => "task_name",
+        }
+    }
+}
+
 pub fn get_directory() -> PathBuf {
-    if let Some(proj_dirs) = ProjectDirs::from("com", "lakoliu",  "Furtherance") {
+    if let Some(proj_dirs) = ProjectDirs::from("com", "lakoliu", "Furtherance") {
         let mut path = PathBuf::from(proj_dirs.data_dir());
         create_dir_all(path.clone()).expect("Unable to create database directory");
         path.extend(&["furtherance.db"]);
-        return path
+        return path;
     }
     PathBuf::new()
 }
@@ -58,33 +152,39 @@ pub fn upgrade_old_db() -> Result<()> {
     // Update from old DB w/o tags
     let conn = Connection::open(get_directory())?;
 
-    conn.execute(
-        "ALTER TABLE tasks ADD COLUMN tags TEXT DEFAULT ' '",
-        [],
-    )?;
+    conn.execute("ALTER TABLE tasks ADD COLUMN tags TEXT DEFAULT ' '", [])?;
 
     Ok(())
 }
 
-pub fn db_write(task_name: &str,
-                start_time: DateTime<Local>,
-                stop_time: DateTime<Local>,
-                tags: String) -> Result<()> {
+pub fn db_write(
+    task_name: &str,
+    start_time: DateTime<Local>,
+    stop_time: DateTime<Local>,
+    tags: String,
+) -> Result<()> {
     // Write data into database
     let conn = Connection::open(get_directory())?;
 
     conn.execute(
         "INSERT INTO tasks (task_name, start_time, stop_time, tags) values (?1, ?2, ?3, ?4)",
-        &[&task_name.to_string(), &start_time.to_rfc3339(), &stop_time.to_rfc3339(), &tags],
+        &[
+            &task_name.to_string(),
+            &start_time.to_rfc3339(),
+            &stop_time.to_rfc3339(),
+            &tags,
+        ],
     )?;
 
     Ok(())
 }
 
-pub fn write_autosave(task_name: &str,
-                start_time: &str,
-                stop_time: &str,
-                tags: &str) -> Result<()> {
+pub fn write_autosave(
+    task_name: &str,
+    start_time: &str,
+    stop_time: &str,
+    tags: &str,
+) -> Result<()> {
     // Write data into database
     let conn = Connection::open(get_directory())?;
 
@@ -96,11 +196,18 @@ pub fn write_autosave(task_name: &str,
     Ok(())
 }
 
-pub fn retrieve() -> Result<Vec<Task>, rusqlite::Error> {
+pub fn retrieve(sort: TaskSort, order: SortOrder) -> Result<Vec<Task>, rusqlite::Error> {
     // Retrieve all tasks from the database
     let conn = Connection::open(get_directory())?;
 
-    let mut query = conn.prepare("SELECT * FROM tasks ORDER BY start_time")?;
+    let mut query = conn.prepare(
+        format!(
+            "SELECT * FROM tasks ORDER BY {0} {1}",
+            sort.to_sqlite(),
+            order.to_sqlite()
+        )
+        .as_str(),
+    )?;
     let task_iter = query.query_map([], |row| {
         Ok(Task {
             id: row.get(0)?,
@@ -117,11 +224,27 @@ pub fn retrieve() -> Result<Vec<Task>, rusqlite::Error> {
     }
 
     Ok(tasks_vec)
+}
 
+/// Exports the database as CSV.
+/// The delimiter parameter is interpreted as a ASCII character.
+pub fn export_as_csv(sort: TaskSort, order: SortOrder, delimiter: u8) -> anyhow::Result<String> {
+    let mut csv_writer = csv::WriterBuilder::new()
+        .delimiter(delimiter)
+        .from_writer(vec![]);
+    let tasks = retrieve(sort, order)?;
+
+    for task in tasks {
+        csv_writer.serialize(task)?;
+    }
+
+    csv_writer.flush()?;
+
+    Ok(String::from_utf8(csv_writer.into_inner()?)?)
 }
 
 // pub fn retrieve_date_range() -> Result<Vec<Task>, rusqlite::Error> {
-    // Retrieve all tasks from the database
+// Retrieve all tasks from the database
 //     let conn = Connection::open(get_directory())?;
 
 //     let mut query = conn.prepare("SELECT * FROM tasks ORDER BY start_time")?;
@@ -151,7 +274,7 @@ pub fn update_start_time(id: i32, start_time: String) -> Result<()> {
 
     conn.execute(
         "UPDATE tasks SET start_time = (?1) WHERE id = (?2)",
-        &[&start_time, &id.to_string()]
+        &[&start_time, &id.to_string()],
     )?;
 
     Ok(())
@@ -162,7 +285,7 @@ pub fn update_stop_time(id: i32, stop_time: String) -> Result<()> {
 
     conn.execute(
         "UPDATE tasks SET stop_time = (?1) WHERE id = (?2)",
-        &[&stop_time, &id.to_string()]
+        &[&stop_time, &id.to_string()],
     )?;
 
     Ok(())
@@ -173,7 +296,7 @@ pub fn update_task_name(id: i32, task_name: String) -> Result<()> {
 
     conn.execute(
         "UPDATE tasks SET task_name = (?1) WHERE id = (?2)",
-        &[&task_name, &id.to_string()]
+        &[&task_name, &id.to_string()],
     )?;
 
     Ok(())
@@ -184,7 +307,7 @@ pub fn update_tags(id: i32, tags: String) -> Result<()> {
 
     conn.execute(
         "UPDATE tasks SET tags = (?1) WHERE id = (?2)",
-        &[&tags, &id.to_string()]
+        &[&tags, &id.to_string()],
     )?;
 
     Ok(())
@@ -195,8 +318,7 @@ pub fn get_list_by_id(id_list: Vec<i32>) -> Result<Vec<Task>, rusqlite::Error> {
     let mut tasks_vec: Vec<Task> = Vec::new();
 
     for id in id_list {
-        let mut query = conn.prepare(
-            "SELECT * FROM tasks WHERE id = :id;")?;
+        let mut query = conn.prepare("SELECT * FROM tasks WHERE id = :id;")?;
         let task_iter = query.query_map(&[(":id", &id.to_string())], |row| {
             Ok(Task {
                 id: row.get(0)?,
@@ -247,7 +369,7 @@ pub fn delete_all() -> Result<()> {
     // Delete everything from the database
     let conn = Connection::open(get_directory())?;
 
-    conn.execute("delete from tasks",[],)?;
+    conn.execute("delete from tasks", [])?;
 
     Ok(())
 }
diff --git a/src/furtherance.gresource.xml b/src/furtherance.gresource.xml
index bb1a9fb..bd94858 100755
--- a/src/furtherance.gresource.xml
+++ b/src/furtherance.gresource.xml
@@ -9,6 +9,7 @@
     <file>gtk/tasks_group.ui</file>
     <file>gtk/tasks_page.ui</file>
     <file>gtk/task_row.ui</file>
+    <file>gtk/dialogs.ui</file>
     <file>gtk/window.ui</file>
   </gresource>
 </gresources>
diff --git a/src/gtk/dialogs.ui b/src/gtk/dialogs.ui
new file mode 100644
index 0000000..a4c1e6f
--- /dev/null
+++ b/src/gtk/dialogs.ui
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+
+  <object class="GtkDialog" id="dialog_csv_export">
+    <property name="use-header-bar">1</property>
+    <property name="modal">true</property>
+    <property name="title" translatable="yes">Export as CSV</property>
+    <child type="action">
+      <object class="GtkButton" id="csv_export_cancelbutton">
+        <property name="label" translatable="yes">Cancel</property>
+      </object>
+    </child>
+    <child type="action">
+      <object class="GtkButton" id="csv_export_applybutton">
+        <property name="label" translatable="yes">Export</property>
+        <style>
+          <class name="suggested-action" />
+        </style>
+      </object>
+    </child>
+    <action-widgets>
+      <action-widget response="cancel">csv_export_cancelbutton</action-widget>
+      <action-widget response="apply" default="true">csv_export_applybutton</action-widget>
+    </action-widgets>
+    <child>
+      <object class="AdwClamp">
+        <property name="maximum-size">800</property>
+        <property name="tightening-threshold">600</property>
+        <property name="hexpand">true</property>
+        <property name="vexpand">false</property>
+        <property name="valign">fill</property>
+        <property name="halign">fill</property>
+        <child>
+          <object class="GtkBox">
+            <property name="orientation">vertical</property>
+            <property name="spacing">24</property>
+            <property name="margin-start">12</property>
+            <property name="margin-end">12</property>
+            <property name="margin-top">12</property>
+            <property name="margin-bottom">12</property>
+            <child>
+              <object class="GtkBox">
+                <property name="orientation">horizontal</property>
+                <property name="spacing">12</property>
+                <child>
+                  <object class="GtkLabel" id="csv_export_chosenfile_label">
+                    <property translatable="yes" name="label"> - no file selected - </property>
+                    <property name="hexpand">true</property>
+                    <property name="halign">start</property>
+                    <property name="ellipsize">start</property>
+                    <style>
+                      <class name="dim-label" />
+                    </style>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkButton" id="csv_export_filechooser_button">
+                    <property name="icon-name">folder-open-symbolic</property>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="AdwPreferencesGroup">
+                <property name="title" translatable="yes">CSV export preferences</property>
+                <property name="halign">fill</property>
+                <child>
+                  <object class="AdwComboRow" id="csv_export_tasksort_row">
+                    <property name="title">Sort by</property>
+                    <property name="model">
+                      <object class="AdwEnumListModel">
+                        <property name="enum-type">TaskSort</property>
+                      </object>
+                    </property>
+                    <property name="expression">
+                      <lookup type="AdwEnumListItem" name="nick" />
+                    </property>
+                  </object>
+                </child>
+                <child>
+                  <object class="AdwComboRow" id="csv_export_sortorder_row">
+                    <property name="title">Sort order</property>
+                    <property name="model">
+                      <object class="AdwEnumListModel">
+                        <property name="enum-type">SortOrder</property>
+                      </object>
+                    </property>
+                    <property name="expression">
+                      <lookup type="AdwEnumListItem" name="nick" />
+                    </property>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </object>
+</interface>
\ No newline at end of file
diff --git a/src/gtk/window.ui b/src/gtk/window.ui
index 174acc0..25b81ca 100755
--- a/src/gtk/window.ui
+++ b/src/gtk/window.ui
@@ -101,6 +101,10 @@
         <attribute name="action">app.delete-history</attribute>
       </item>
       <item>
+        <attribute name="label" translatable="yes">_Export as CSV</attribute>
+        <attribute name="action">app.export-csv</attribute>
+      </item>
+      <item>
         <attribute name="label" translatable="yes">_About Furtherance</attribute>
         <attribute name="action">app.about</attribute>
       </item>
diff --git a/src/ui/report.rs b/src/ui/report.rs
index 4fcfc51..c91806d 100644
--- a/src/ui/report.rs
+++ b/src/ui/report.rs
@@ -15,16 +15,16 @@
 // along with this program.  If not, see <https://www.gnu.org/licenses/>.
 
 use adw::subclass::prelude::*;
+use chrono::{offset::TimeZone, Date, DateTime, Datelike, Duration, Local, NaiveDate};
 use gettextrs::*;
 use glib::clone;
 use gtk::subclass::prelude::*;
 use gtk::{glib, prelude::*, CompositeTemplate};
-use chrono::{DateTime, NaiveDate, Local, Duration, Date, Datelike, offset::TimeZone};
 use itertools::Itertools;
 
-use crate::FurtheranceApplication;
+use crate::database::{self, SortOrder, TaskSort};
 use crate::ui::FurtheranceWindow;
-use crate::database;
+use crate::FurtheranceApplication;
 
 mod imp {
     use super::*;
@@ -81,7 +81,6 @@ mod imp {
     }
 
     impl ObjectImpl for FurReport {
-
         fn constructed(&self, obj: &Self::Type) {
             obj.setup_widgets();
             self.parent_constructed(obj);
@@ -119,43 +118,49 @@ impl FurReport {
         imp.range_combo.set_active_id(Some("week_item"));
         imp.filter_combo.set_active_id(Some("tasks_item"));
 
-        imp.range_combo.connect_changed(clone!(@weak self as this => move |combo|{
-            let imp = imp::FurReport::from_instance(&this);
-            if combo.active_id().unwrap() != "date_range_item" {
-                imp.date_range_box.set_visible(false);
-                this.refresh_report();
-            } else {
-                imp.date_range_box.set_visible(true);
-            }
-        }));
-
-        imp.filter_check.connect_toggled(clone!(@weak self as this => move |_|{
-            let imp = imp::FurReport::from_instance(&this);
-            if imp.filter_box.get_visible() {
-                imp.filter_box.set_visible(false);
-            } else {
-                imp.filter_box.set_visible(true);
-            }
-        }));
-
-        imp.filter_combo.connect_changed(clone!(@weak self as this => move |combo|{
-            let imp = imp::FurReport::from_instance(&this);
-            if combo.active_id().unwrap() == "tasks_item" {
-                imp.filter_entry.set_placeholder_text(Some(&gettext("Task, Task 2")));
-            } else {
-                imp.filter_entry.set_placeholder_text(Some(&gettext("tag, tag 2")));
-            }
-        }));
+        imp.range_combo
+            .connect_changed(clone!(@weak self as this => move |combo|{
+                let imp = imp::FurReport::from_instance(&this);
+                if combo.active_id().unwrap() != "date_range_item" {
+                    imp.date_range_box.set_visible(false);
+                    this.refresh_report();
+                } else {
+                    imp.date_range_box.set_visible(true);
+                }
+            }));
 
-        imp.refresh_btn.connect_clicked(clone!(@weak self as this => move |_|{
-            this.refresh_report();
-        }));
+        imp.filter_check
+            .connect_toggled(clone!(@weak self as this => move |_|{
+                let imp = imp::FurReport::from_instance(&this);
+                if imp.filter_box.get_visible() {
+                    imp.filter_box.set_visible(false);
+                } else {
+                    imp.filter_box.set_visible(true);
+                }
+            }));
+
+        imp.filter_combo
+            .connect_changed(clone!(@weak self as this => move |combo|{
+                let imp = imp::FurReport::from_instance(&this);
+                if combo.active_id().unwrap() == "tasks_item" {
+                    imp.filter_entry.set_placeholder_text(Some(&gettext("Task, Task 2")));
+                } else {
+                    imp.filter_entry.set_placeholder_text(Some(&gettext("tag, tag 2")));
+                }
+            }));
+
+        imp.refresh_btn
+            .connect_clicked(clone!(@weak self as this => move |_|{
+                this.refresh_report();
+            }));
 
         let renderer = gtk::CellRendererText::new();
-        let task_column = gtk::TreeViewColumn::with_attributes(&gettext("Task"), &renderer, &[("text", 0)]);
+        let task_column =
+            gtk::TreeViewColumn::with_attributes(&gettext("Task"), &renderer, &[("text", 0)]);
         task_column.set_expand(true);
         task_column.set_resizable(true);
-        let duration_column = gtk::TreeViewColumn::with_attributes(&gettext("Duration"), &renderer, &[("text", 1)]);
+        let duration_column =
+            gtk::TreeViewColumn::with_attributes(&gettext("Duration"), &renderer, &[("text", 1)]);
         duration_column.set_expand(false);
         duration_column.set_resizable(true);
         imp.results_tree.append_column(&task_column);
@@ -172,8 +177,7 @@ impl FurReport {
 
         let results_model = gtk::TreeStore::new(&[String::static_type(), String::static_type()]);
 
-        let mut task_list = database::retrieve().unwrap();
-        task_list.reverse();
+        let task_list = database::retrieve(TaskSort::StartTime, SortOrder::Descending).unwrap();
 
         // Get date range
         let active_range = imp.range_combo.active_id().unwrap();
@@ -192,26 +196,27 @@ impl FurReport {
         } else if active_range == "year_item" {
             range_start_date = today - Duration::days(365);
         } else {
-            let input_start_date = NaiveDate::parse_from_str(&imp.start_date_entry.text(), "%m/%d/%Y");
+            let input_start_date =
+                NaiveDate::parse_from_str(&imp.start_date_entry.text(), "%m/%d/%Y");
             let input_end_date = NaiveDate::parse_from_str(&imp.end_date_entry.text(), "%m/%d/%Y");
             // Check if user entered dates properly
             if let Err(_) = input_start_date {
                 imp.format_error.set_visible(true);
                 results_model.clear();
                 imp.results_tree.set_model(Some(&results_model));
-                return
+                return;
             }
             if let Err(_) = input_end_date {
                 imp.format_error.set_visible(true);
                 results_model.clear();
                 imp.results_tree.set_model(Some(&results_model));
-                return
+                return;
             }
             // Start date cannot be after end date
             if (input_end_date.unwrap() - input_start_date.unwrap()).num_days() < 0 {
                 imp.start_end_error.set_visible(true);
                 results_model.clear();
-                return
+                return;
             }
             range_start_date = Local.from_local_date(&input_start_date.unwrap()).unwrap();
             range_end_date = Local.from_local_date(&input_end_date.unwrap()).unwrap();
@@ -222,8 +227,12 @@ impl FurReport {
         let mut user_chosen_tags: Vec<String> = Vec::new();
         let mut only_this_tag = false;
         for task in task_list {
-            let start = DateTime::parse_from_rfc3339(&task.start_time).unwrap().with_timezone(&Local);
-            let stop = DateTime::parse_from_rfc3339(&task.stop_time).unwrap().with_timezone(&Local);
+            let start = DateTime::parse_from_rfc3339(&task.start_time)
+                .unwrap()
+                .with_timezone(&Local);
+            let stop = DateTime::parse_from_rfc3339(&task.stop_time)
+                .unwrap()
+                .with_timezone(&Local);
             // Check if start time is in date range and if not remove it from task_list
             let start_date = start.date();
             if start_date >= range_start_date && start_date <= range_end_date {
@@ -240,7 +249,8 @@ impl FurReport {
                         // Handle duplicate tasks
                         split_tasks = split_tasks.into_iter().unique().collect();
                         // Lowercase tags
-                        let lower_tasks: Vec<String> = split_tasks.iter().map(|x| x.to_lowercase()).collect();
+                        let lower_tasks: Vec<String> =
+                            split_tasks.iter().map(|x| x.to_lowercase()).collect();
 
                         if lower_tasks.contains(&task.task_name.to_lowercase()) {
                             let duration = stop - start;
@@ -248,7 +258,6 @@ impl FurReport {
                             tasks_in_range.push((task, duration));
                             total_time += duration;
                         }
-
                     } else if imp.filter_combo.active_id().unwrap() == "tags_item" {
                         // Split user chosen tags
                         let chosen_tasgs = imp.filter_entry.text();
@@ -287,22 +296,20 @@ impl FurReport {
             }
         }
 
-        let all_tasks_iter:gtk::TreeIter;
+        let all_tasks_iter: gtk::TreeIter;
         if tasks_in_range.is_empty() {
-            all_tasks_iter = results_model.insert_with_values(None,
-                                                                None,
-                                                                &[
-                                                                    (0, &gettext("No Results")),
-                                                                    (1, &"")
-                                                                ]);
+            all_tasks_iter = results_model.insert_with_values(
+                None,
+                None,
+                &[(0, &gettext("No Results")), (1, &"")],
+            );
         } else {
             let total_time_str = FurReport::format_duration(total_time);
-            all_tasks_iter = results_model.insert_with_values(None,
-                                                                    None,
-                                                                    &[
-                                                                        (0, &gettext("All Results")),
-                                                                        (1, &total_time_str)
-                                                                    ]);
+            all_tasks_iter = results_model.insert_with_values(
+                None,
+                None,
+                &[(0, &gettext("All Results")), (1, &total_time_str)],
+            );
         }
 
         if imp.sort_by_task.is_active() {
@@ -368,12 +375,12 @@ impl FurReport {
                     let _child_iter = results_model.insert_with_values(
                         Some(&header_iter),
                         None,
-                        &[(0, &task), (1, &FurReport::format_duration(task_duration))]
+                        &[(0, &task), (1, &FurReport::format_duration(task_duration))],
                     );
                 }
                 results_model.set(
                     &header_iter,
-                    &[(0, &stbd.0), (1, &FurReport::format_duration(stbd.1))]
+                    &[(0, &stbd.0), (1, &FurReport::format_duration(stbd.1))],
                 );
             }
         } else if imp.sort_by_tag.is_active() {
@@ -432,13 +439,15 @@ impl FurReport {
             }
 
             for mut stbd in sorted_tasks_by_duration {
-                if !only_this_tag || (only_this_tag && user_chosen_tags.contains(&stbd.0[1..].to_string())) {
+                if !only_this_tag
+                    || (only_this_tag && user_chosen_tags.contains(&stbd.0[1..].to_string()))
+                {
                     let header_iter = results_model.append(Some(&all_tasks_iter));
                     for (task, task_duration) in stbd.2 {
                         let _child_iter = results_model.insert_with_values(
                             Some(&header_iter),
                             None,
-                            &[(0, &task), (1, &FurReport::format_duration(task_duration))]
+                            &[(0, &task), (1, &FurReport::format_duration(task_duration))],
                         );
                     }
                     if stbd.0 == "#" {
@@ -446,7 +455,7 @@ impl FurReport {
                     }
                     results_model.set(
                         &header_iter,
-                        &[(0, &stbd.0), (1, &FurReport::format_duration(stbd.1))]
+                        &[(0, &stbd.0), (1, &FurReport::format_duration(stbd.1))],
                     );
                 }
             }
@@ -459,11 +468,10 @@ impl FurReport {
     }
 
     fn format_duration(total_time: i64) -> String {
-         // Format total time to readable string
+        // Format total time to readable string
         let h = total_time / 3600;
         let m = total_time % 3600 / 60;
         let s = total_time % 60;
         format!("{:02}:{:02}:{:02}", h, m, s)
     }
 }
-
diff --git a/src/ui/tasks_page.rs b/src/ui/tasks_page.rs
index 7fb6e63..911b005 100755
--- a/src/ui/tasks_page.rs
+++ b/src/ui/tasks_page.rs
@@ -14,16 +14,16 @@
 // You should have received a copy of the GNU General Public License
 // along with this program.  If not, see <https://www.gnu.org/licenses/>.
 
+use adw::prelude::{PreferencesGroupExt, PreferencesPageExt};
 use adw::subclass::prelude::*;
-use adw::prelude::{PreferencesPageExt, PreferencesGroupExt};
+use chrono::{DateTime, Duration, Local};
 use gettextrs::*;
 use gtk::subclass::prelude::*;
 use gtk::{glib, prelude::*};
-use chrono::{DateTime, Local, Duration};
 
-use crate::ui::FurTasksGroup;
-use crate::database;
+use crate::database::{self, SortOrder, TaskSort};
 use crate::settings_manager;
+use crate::ui::FurTasksGroup;
 
 mod imp {
     use super::*;
@@ -37,7 +37,6 @@ mod imp {
         pub all_groups: RefCell<Vec<FurTasksGroup>>,
     }
 
-
     #[glib::object_subclass]
     impl ObjectSubclass for FurTasksPage {
         const NAME: &'static str = "FurTasksPage";
@@ -88,10 +87,8 @@ impl FurTasksPage {
     pub fn build_task_list(&self) {
         let imp = imp::FurTasksPage::from_instance(&self);
 
-        let mut tasks_list = database::retrieve().unwrap();
+        let tasks_list = database::retrieve(TaskSort::StartTime, SortOrder::Descending).unwrap();
 
-        // Reversing chronological order of tasks_list
-        tasks_list.reverse();
         let mut uniq_date_list: Vec<String> = Vec::new();
         let mut same_date_list: Vec<database::Task> = Vec::new();
         let mut tasks_sorted_by_day: Vec<Vec<database::Task>> = Vec::new();
@@ -144,7 +141,7 @@ impl FurTasksPage {
             let group = FurTasksGroup::new();
             if uniq_date_list[i] == today {
                 group.set_title(&gettext("Today"));
-            } else if uniq_date_list[i] == yesterday{
+            } else if uniq_date_list[i] == yesterday {
                 group.set_title(&gettext("Yesterday"));
             } else {
                 group.set_title(&uniq_date_list[i]);
@@ -171,4 +168,3 @@ impl FurTasksPage {
         }
     }
 }
-
diff --git a/src/ui/window.rs b/src/ui/window.rs
index 133da09..d02d1b5 100755
--- a/src/ui/window.rs
+++ b/src/ui/window.rs
@@ -14,31 +14,34 @@
 // You should have received a copy of the GNU General Public License
 // along with this program.  If not, see <https://www.gnu.org/licenses/>.
 
+use adw::prelude::*;
 use adw::subclass::prelude::AdwApplicationWindowImpl;
+use chrono::{offset::TimeZone, DateTime, Duration as ChronDur, Local, NaiveDateTime, ParseError};
+use dbus::blocking::Connection;
+use directories::ProjectDirs;
 use gettextrs::*;
-use gtk::prelude::*;
+use glib::{clone, timeout_add_local};
 use gtk::subclass::prelude::*;
 use gtk::{gio, glib, CompositeTemplate};
-use glib::{clone, timeout_add_local};
-use std::time::Duration;
-use std::sync::Mutex;
-use std::rc::Rc;
-use std::cell::RefCell;
-use chrono::{DateTime, Local, NaiveDateTime, ParseError, Duration as ChronDur, offset::TimeZone};
-use dbus::blocking::Connection;
 use itertools::Itertools;
-use std::fs::{File, create_dir_all, remove_file};
-use std::io::{self, BufWriter, Write, BufReader, BufRead};
-use directories::ProjectDirs;
+use std::cell::RefCell;
+use std::convert::TryFrom;
+use std::fs::{create_dir_all, remove_file, File};
+use std::io::{self, BufRead, BufReader, BufWriter, Write};
 use std::path::PathBuf;
+use std::rc::Rc;
+use std::sync::Mutex;
+use std::time::Duration;
 
+use crate::config;
+use crate::database::{self, SortOrder, TaskSort};
+use crate::settings_manager;
 use crate::ui::FurHistoryBox;
 use crate::FurtheranceApplication;
-use crate::database;
-use crate::settings_manager;
-use crate::config;
 
 mod imp {
+    use crate::database::{SortOrder, TaskSort};
+
     use super::*;
 
     #[derive(Debug, Default, CompositeTemplate)]
@@ -68,6 +71,9 @@ mod imp {
         pub running: Mutex<bool>,
         pub pomodoro_continue: Mutex<bool>,
         pub idle_dialog: Mutex<gtk::MessageDialog>,
+
+        // We have to keep a reference to the current popped up filechooser dialog
+        pub filechooser: RefCell<gtk::FileChooserNative>,
     }
 
     #[glib::object_subclass]
@@ -78,6 +84,8 @@ mod imp {
 
         fn class_init(klass: &mut Self::Class) {
             FurHistoryBox::static_type();
+            TaskSort::static_type();
+            SortOrder::static_type();
             Self::bind_template(klass);
         }
 
@@ -133,7 +141,8 @@ impl FurtheranceWindow {
         let imp = imp::FurtheranceWindow::from_instance(self);
 
         if *imp.subtract_idle.lock().unwrap() {
-            let idle_start = DateTime::parse_from_rfc3339(&imp.idle_start_time.lock().unwrap()).unwrap();
+            let idle_start =
+                DateTime::parse_from_rfc3339(&imp.idle_start_time.lock().unwrap()).unwrap();
             stop_time = idle_start.with_timezone(&Local);
             *imp.subtract_idle.lock().unwrap() = false;
         }
@@ -186,16 +195,17 @@ impl FurtheranceWindow {
         let start_time = Rc::new(RefCell::new(Local::now()));
         let stop_time = Rc::new(RefCell::new(Local::now()));
 
-        imp.task_input.connect_changed(clone!(@weak self as this => move |task_input| {
-            let imp2 = imp::FurtheranceWindow::from_instance(&this);
-            let task_input_text = task_input.text();
-            let split_tags: Vec<&str> = task_input_text.trim().split("#").collect();
-            if split_tags[0].trim().is_empty() {
-                imp2.start_button.set_sensitive(false);
-            } else {
-                imp2.start_button.set_sensitive(true);
-            }
-        }));
+        imp.task_input
+            .connect_changed(clone!(@weak self as this => move |task_input| {
+                let imp2 = imp::FurtheranceWindow::from_instance(&this);
+                let task_input_text = task_input.text();
+                let split_tags: Vec<&str> = task_input_text.trim().split("#").collect();
+                if split_tags[0].trim().is_empty() {
+                    imp2.start_button.set_sensitive(false);
+                } else {
+                    imp2.start_button.set_sensitive(true);
+                }
+            }));
 
         imp.start_button.connect_clicked(clone!(@weak self as this => move |button| {
             let imp2 = imp::FurtheranceWindow::from_instance(&this);
@@ -502,11 +512,13 @@ impl FurtheranceWindow {
     fn get_idle_time(&self) -> Result<u64, Box<dyn std::error::Error>> {
         let c = Connection::new_session()?;
 
-        let p = c.with_proxy("org.gnome.Mutter.IdleMonitor",
+        let p = c.with_proxy(
+            "org.gnome.Mutter.IdleMonitor",
             "/org/gnome/Mutter/IdleMonitor/Core",
-            Duration::from_millis(5000)
+            Duration::from_millis(5000),
         );
-        let (idle_time,): (u64,) = p.method_call("org.gnome.Mutter.IdleMonitor", "GetIdletime", ())?;
+        let (idle_time,): (u64,) =
+            p.method_call("org.gnome.Mutter.IdleMonitor", "GetIdletime", ())?;
 
         Ok(idle_time / 1000)
     }
@@ -521,20 +533,20 @@ impl FurtheranceWindow {
         // If user was idle and has now returned...
         if idle_time < (settings_manager::get_int("idle-time") * 60) as u64
             && *imp.idle_time_reached.lock().unwrap()
-            && !*imp.idle_notified.lock().unwrap() {
-
-                *imp.idle_notified.lock().unwrap() = true;
-                self.resume_from_idle();
+            && !*imp.idle_notified.lock().unwrap()
+        {
+            *imp.idle_notified.lock().unwrap() = true;
+            self.resume_from_idle();
         }
         *imp.stored_idle.lock().unwrap() = idle_time;
 
         // If user is idle but has not returned...
         if *imp.stored_idle.lock().unwrap() >= (settings_manager::get_int("idle-time") * 60) as u64
-            && !*imp.idle_time_reached.lock().unwrap() {
-
+            && !*imp.idle_time_reached.lock().unwrap()
+        {
             *imp.idle_time_reached.lock().unwrap() = true;
-            let true_idle_start_time = Local::now() -
-                ChronDur::seconds((settings_manager::get_int("idle-time") * 60) as i64);
+            let true_idle_start_time = Local::now()
+                - ChronDur::seconds((settings_manager::get_int("idle-time") * 60) as i64);
             *imp.idle_start_time.lock().unwrap() = true_idle_start_time.to_rfc3339();
         }
     }
@@ -543,14 +555,21 @@ impl FurtheranceWindow {
         let imp = imp::FurtheranceWindow::from_instance(self);
 
         let resume_time = Local::now();
-        let idle_start = DateTime::parse_from_rfc3339(&imp.idle_start_time.lock().unwrap()).unwrap();
+        let idle_start =
+            DateTime::parse_from_rfc3339(&imp.idle_start_time.lock().unwrap()).unwrap();
         let idle_start = idle_start.with_timezone(&Local);
         let idle_time = resume_time - idle_start;
         let idle_time = idle_time.num_seconds();
         let h = idle_time / 60 / 60;
         let m = (idle_time / 60) - (h * 60);
         let s = idle_time - (m * 60);
-        let idle_time_str = format!("{}{:02}:{:02}:{:02}", gettext("You have been idle for "), h, m, s);
+        let idle_time_str = format!(
+            "{}{:02}:{:02}:{:02}",
+            gettext("You have been idle for "),
+            h,
+            m,
+            s
+        );
         let question_str = gettext("\nWould you like to discard that time, or continue the clock?");
         let idle_time_msg = format!("{}{}", idle_time_str, question_str);
 
@@ -559,11 +578,14 @@ impl FurtheranceWindow {
             gtk::DialogFlags::MODAL,
             gtk::MessageType::Warning,
             gtk::ButtonsType::None,
-            Some(&format!("<span size='x-large' weight='bold'>{}</span>", &gettext("Idle"))),
+            Some(&format!(
+                "<span size='x-large' weight='bold'>{}</span>",
+                &gettext("Idle")
+            )),
         );
         dialog.add_buttons(&[
             (&gettext("Discard"), gtk::ResponseType::Reject),
-            (&gettext("Continue"), gtk::ResponseType::Accept)
+            (&gettext("Continue"), gtk::ResponseType::Accept),
         ]);
         dialog.set_secondary_text(Some(&idle_time_msg));
 
@@ -594,11 +616,14 @@ impl FurtheranceWindow {
             gtk::DialogFlags::MODAL,
             gtk::MessageType::Warning,
             gtk::ButtonsType::None,
-            Some(&format!("<span size='x-large' weight='bold'>{}</span>", &gettext("Time's up!"))),
+            Some(&format!(
+                "<span size='x-large' weight='bold'>{}</span>",
+                &gettext("Time's up!")
+            )),
         );
         dialog.add_buttons(&[
             (&gettext("Continue"), gtk::ResponseType::Accept),
-            (&gettext("Stop"), gtk::ResponseType::Reject)
+            (&gettext("Stop"), gtk::ResponseType::Reject),
         ]);
 
         let app = FurtheranceApplication::default();
@@ -652,7 +677,7 @@ impl FurtheranceWindow {
 
     fn get_autosave_path() -> PathBuf {
         let mut path = PathBuf::new();
-        if let Some(proj_dirs) = ProjectDirs::from("com", "lakoliu",  "Furtherance") {
+        if let Some(proj_dirs) = ProjectDirs::from("com", "lakoliu", "Furtherance") {
             path = PathBuf::from(proj_dirs.data_dir());
             create_dir_all(path.clone()).expect("Unable to create autosave directory");
             path.extend(&["furtherance_autosave.txt"]);
@@ -694,9 +719,9 @@ impl FurtheranceWindow {
                 gtk::ButtonsType::Ok,
                 &gettext("Autosave Restored"),
             );
-            dialog.set_secondary_text(Some(
-                &gettext("Furtherance shut down improperly. An autosave was restored.")
-            ));
+            dialog.set_secondary_text(Some(&gettext(
+                "Furtherance shut down improperly. An autosave was restored.",
+            )));
 
             dialog.connect_response(clone!(
                 @weak self as this,
@@ -773,6 +798,118 @@ impl FurtheranceWindow {
             imp.watch.set_text("00:00:00");
         }
     }
+
+    pub async fn export_csv_to_file(
+        sort: TaskSort,
+        order: SortOrder,
+        file: &gio::File,
+    ) -> anyhow::Result<()> {
+        async fn overwrite_file_future(file: &gio::File, bytes: Vec<u8>) -> anyhow::Result<()> {
+            let output_stream = file
+                .replace_future(
+                    None,
+                    false,
+                    gio::FileCreateFlags::REPLACE_DESTINATION,
+                    glib::PRIORITY_DEFAULT,
+                )
+                .await?;
+
+            output_stream
+                .write_all_future(bytes, glib::PRIORITY_DEFAULT)
+                .await
+                .map_err(|e| anyhow::anyhow!(e.1))?;
+            output_stream.close_future(glib::PRIORITY_DEFAULT).await?;
+
+            Ok(())
+        }
+
+        let csv = database::export_as_csv(sort, order, b',')?;
+        overwrite_file_future(file, csv.into_bytes()).await
+    }
+
+    pub fn open_csv_export_dialog(&self) {
+        let builder = gtk::Builder::from_resource("/com/lakoliu/Furtherance/gtk/dialogs.ui");
+        let dialog = builder.object::<gtk::Dialog>("dialog_csv_export").unwrap();
+        let tasksort_row = builder
+            .object::<adw::ComboRow>("csv_export_tasksort_row")
+            .unwrap();
+        let sortorder_row = builder
+            .object::<adw::ComboRow>("csv_export_sortorder_row")
+            .unwrap();
+        let filechooser_button = builder
+            .object::<gtk::Button>("csv_export_filechooser_button")
+            .unwrap();
+        let chosenfile_label = builder
+            .object::<gtk::Label>("csv_export_chosenfile_label")
+            .unwrap();
+
+        dialog.set_transient_for(Some(self));
+
+        let filefilter = gtk::FileFilter::new();
+        filefilter.add_mime_type("text/csv");
+        filefilter.add_pattern("*.csv");
+
+        let filechooser = gtk::FileChooserNative::builder()
+            .title(&gettext("Create or choose a CSV file"))
+            .modal(true)
+            .transient_for(self)
+            .action(gtk::FileChooserAction::Save)
+            .accept_label(&gettext("Accept"))
+            .cancel_label(&gettext("Cancel"))
+            .select_multiple(false)
+            .filter(&filefilter)
+            .build();
+
+        filechooser.set_current_name("data.csv");
+
+        filechooser_button.connect_clicked(
+            clone!(@weak self as window, @weak filechooser, @weak dialog => move |_| {
+                dialog.hide();
+                filechooser.show();
+            }),
+        );
+
+        filechooser.connect_response(
+            clone!(@weak dialog, @weak chosenfile_label => move |filechooser, response| {
+                if response == gtk::ResponseType::Accept {
+                    if let Some(path) = filechooser.file().and_then(|file| file.path()) {
+                        chosenfile_label.set_label(&path.to_string_lossy());
+                    } else {
+                        chosenfile_label.set_label(&gettext(" - no file selected - "));
+                    }
+                }
+
+                dialog.show();
+            }),
+        );
+
+        dialog.connect_response(clone!(@weak self as window, @weak filechooser, @weak tasksort_row, @weak sortorder_row => move |dialog, response| {
+            match response {
+                gtk::ResponseType::Apply => {
+                    let sort = TaskSort::try_from(tasksort_row.selected()).unwrap_or_default();
+                    let order = SortOrder::try_from(sortorder_row.selected()).unwrap_or_default();
+
+                    if let Some(file) = filechooser.file() {
+                        glib::MainContext::default().spawn_local(clone!(@strong window, @strong file => async move {
+                            if let Err(e) = FurtheranceWindow::export_csv_to_file(sort, order, &file).await {
+                                log::error!("replace file {:?} failed, Err {}", file, e);
+                                window.display_toast(&gettext("Exporting as CSV failed."));
+                            } else {
+                                window.display_toast(&gettext("Exported as CSV successfully."));
+                            };
+                        }));
+                    }
+                }
+                _ => {}
+            }
+
+            dialog.close();
+        }));
+
+        *self.imp().filechooser.borrow_mut() = filechooser;
+
+        dialog.show()
+    }
 }
 
 impl Default for FurtheranceWindow {
@@ -784,4 +921,3 @@ impl Default for FurtheranceWindow {
             .unwrap()
     }
 }
-