Newer
Older
minerva / Userland / Applications / Weather / main.jakt
@minerva minerva on 13 Jul 10 KB Initial commit
import jakt::platform::utility { null }
import openweathermap { OpenWeatherMap, Promise, RequestData }
import weather { Icon, Temperature, WeatherData, Coords }
import view { AK, Core, Gfx, GUI, Main, Weather, degree_c }

import extern "LibProtocol/RequestClient.h"
import extern "LibProtocol/Request.h"
import extern "LibURL/URL.h"
import extern "LibGfx/Bitmap.h"
import extern "AK/Span.h"
import extern "LibCore/ConfigFile.h"
import extern "LibCore/ElapsedTimer.h"

import jakt::prelude::prelude

type AK::String implements(ThrowingFromStringLiteral) {
    [[name=from_utf8]]
    extern fn from_string_literal(anon s: StringView) throws -> AK::String
}

type AK::ByteString implements(ThrowingFromStringLiteral) {
    [[name=from_utf8]]
    extern fn from_string_literal(anon s: StringView) throws -> AK::ByteString
}

type AK::StringView implements(FromStringLiteral) {}

fn make_an_rc() throws -> Protocol::RequestClient {
    mut rc: Protocol::RequestClient? = None
    unsafe { cpp { "rc = TRY(Protocol::RequestClient::try_create());" }}
    return rc!
}

fn start_request(anon client: &mut Protocol::RequestClient, anon method: AK::ByteString, anon url: URL::URL) throws -> Protocol::Request {
    mut request: Protocol::Request? = None
    unsafe { cpp { "if (auto r = client->start_request(method, url)) request = *r;" }}
    return request!
}

class ProtocolRequestData : RequestData {
    public request: Protocol::Request
}

// FIXME: Jakt has no async support, this is a hack to get "async" functionality by letting the event loop manage the promises.
struct AsyncContext<T> {
    private promises: [(prelude::String?, Promise<T>)] = []

    fn run(mut this, anon promise: Promise<T>, label: prelude::String? = None, rejects_labels: {prelude::String} = {}) {
        mut new_promises: [(prelude::String?, Promise<T>)] = []
        for x in .promises {
            mut (label, promise) = x
            if promise.is_resolved() {
                try promise.await()
            } else if label is Some(name) and rejects_labels.contains(name) {
                promise.reject(prelude::Error::from_string_literal("Cancelled"))
            } else {
                new_promises.push(x)
            }
        }

        new_promises.push((label, promise))
        .promises = new_promises
    }

    fn event(mut this) {
        mut new_promises: [(prelude::String?, Promise<T>)] = []
        for x in .promises {
            mut (label, promise) = x
            if promise.is_resolved() {
                try promise.await()
            } else {
                new_promises.push(x)
            }
        }
        .promises = new_promises
    }

    fn await_all(mut this) throws {
        for x in .promises {
            mut (_, promise) = x
            try promise.await()
        }
    }
}

[[stores_arguments(client: "return", async: "return")]] // FIXME: We should be able to infer these in pure jakt code.
fn api(
    async: &mut AsyncContext<bool>
    anon key: prelude::String
    anon client: &mut Protocol::RequestClient
) throws -> OpenWeatherMap {
    // FIXME: Default values for RequestClient::start_request() are not brought in by the compiler, so we have to drop down to C++ to call that function.
    return OpenWeatherMap(
        api_key: key
        make_request: fn[&mut client, &mut async](anon u: prelude::String) throws -> RequestData {
            mut request = start_request(&mut client, "GET", URL::URL(u))
            mut promise: Promise<prelude::String> = Promise()

            unsafe {
                cpp {
                    "request->set_buffered_request_finished_callback([promise, &async](auto ok, auto size, auto headers, auto response_code, auto payload) {"
                    "    if (!ok) return promise->reject(Error::from_errno(ENOENT));"
                    "    auto result = AK::ByteString::from_utf8(payload);"
                    "    async.event();"
                    "    if (result.is_error()) promise->reject(result.release_error());"
                    "    promise->resolve(result.release_value());"
                    "});"
                }
            }

            return ProtocolRequestData(promise, request)
        }
    )
}

[[stores_arguments(on_results: async)]]
fn do_incremental_search<T>(
    api: &mut T
    window: &GUI::Window
    query: AK::ByteString
    owm_key: prelude::String
    on_results: &fn(anon results: [prelude::String:Coords]) throws -> void
    async: &mut AsyncContext<bool>
) {
    try {
        async.run(api.find(query: format("{}", query)).map(fn[&on_results](anon results: &mut [prelude::String:Coords]) -> bool {
            try on_results(*results)
            return true
        }))
    } catch e {
        GUI::MessageBox::show_error(&raw window, format("Failed to fetch search results: {}", e))
    }
}

fn update_entry<T>(
    mut entry: IndividualEntryWidget
    api: &mut T
    window: &GUI::Window
    query: AK::ByteString
    first_time: bool = false
) {
    try {
        entry.container!.set_section_label(ak_string(format("Searching for {}...", query)))

        mut data = api.search(query: format("{}", query))
        entry.populate_with(&data)
        if first_time {
            entry.container!.set_view_state(GUI::DynamicWidgetContainer::ViewState::Expanded)
            entry.timer!.on_timeout = fn[entry, &mut api, &window, query]() {
                update_entry(entry, api, &window, query)
            }
            entry.timer!.start(interval_ms: 60000)
        }

        mut forecast = api.forecast(&data)
        entry.populate_with(&forecast)
        entry.invalidate_layout()
    } catch e {
        entry.remove_from_parent()
        GUI::MessageBox::show_error(&raw window, format("Failed to fetch weather data: {}", e))
    }
}

fn main() {
    let args: AK::Span<AK::StringView> = AK::Span()
    mut app = GUI::Application::create(&Main::Arguments(0, null(), args))
    mut async: AsyncContext<bool> = AsyncContext()

    mut config = Core::ConfigFile::open_for_app(&"Weather", Core::ConfigFile::AllowWriting::Yes)
    mut config_key = config.read_entry(group: &"OpenWeatherMap", key: &"APIKey", default_value: &"") as! prelude::String
    let owm_key = match config_key.is_empty() {
        true => {
            mut key = ""
            if GUI::PasswordInputDialog::show(
                parent_window: null()
                text_value: &key
                title: "OpenWeatherMap API Key"
                server: "api.openweathermap.org"
                username: ""
            ) as! GUI::Dialog::ExecResult is OK {
                config.write_entry(group: &"OpenWeatherMap", key: &"APIKey", value: &key)
            } else {
                return 0
            }

            yield key as! prelude::String
        }
        false => config_key
    }

    mut window = GUI::Window(null())
    window.set_title("Weather")
    window.set_icon(GUI::Icon::default_icon("app-weather").bitmap_for_size(16))
    mut view = window.set_main_widget<UnderlyingClassTypeOf<View>>().self()
    window.resize(width: 800, height: 600)

    view.initialize()
    window.show()

    // FIXME: We currently assume try_create() takes the exact same arguments as the constructor.
    //        This is the case in most of the codebase, but there are some exceptions - as seen here.
    mut client = make_an_rc()
    mut owm_api = api(&mut async, owm_key, &mut client)

    mut box = view.searchbox!
    box.on_return_pressed = fn[box, &mut view, &mut owm_api, &window, owm_key]() {
        mut c = view.content!
        mut entry = c.add<UnderlyingClassTypeOf<IndividualEntryWidget>>().self()
        entry.container!.set_view_state(GUI::DynamicWidgetContainer::ViewState::Collapsed)

        try update_entry(entry, api: &mut owm_api, &window, query: box.text(), first_time: true)
    }

    mut bounce_elapsed = Core::ElapsedTimer::start_new(Core::ElapsedTimer::TimerType::Coarse)

    mut on_results = fn[&mut view, &mut owm_api, &window](anon results: [prelude::String:Coords]) throws {
        if not results.is_empty() {
            try view.ensure_search_window(&window).show()
        }

        for (name, coord) in results {
            mut button = view.searchview!.add<UnderlyingClassTypeOf<GUI::Button>>().self()
            button.set_text_alignment(Gfx::TextAlignment::CenterLeft)
            button.set_text(ak_string(name))
            button.on_click = fn[&mut view, &window, &mut owm_api, name](anon _: u32) {
                mut c = view.content!
                mut entry = c.add<UnderlyingClassTypeOf<IndividualEntryWidget>>().self()
                entry.container!.set_view_state(GUI::DynamicWidgetContainer::ViewState::Collapsed)
                try update_entry(entry, api: &mut owm_api, &window, query: name as! ByteString, first_time: true)

                view.searchbox!.set_text("", GUI::TextBox::AllowCallback::No)
                view.searchbox!.set_focus(false, GUI::TextBox::FocusSource::Programmatic)
                view.searchview!.remove_all_children()
                try view.ensure_search_window(&window).hide()
            }
        }
    }

    mut last_query = ""
    mut timer = Core::Timer(null())
    let incremental_search = fn[box, &mut view, &mut last_query, &window, &mut owm_api, owm_key, &mut bounce_elapsed, &on_results, &mut async, &mut timer]() {
        let query = box.text() as! prelude::String
        if query == last_query {
            return
        }

        let elapsed = bounce_elapsed.elapsed_milliseconds()
        if elapsed < 100 {
            timer.start((100 - elapsed) as! i32)
            return
        }

        bounce_elapsed.start()
        timer.stop()

        try view.ensure_search_window(&window).hide()
        mut search_view = view.searchview!
        search_view.remove_all_children()
        view.search_window!.resize(width: search_view.width(), height: 40)

        if query.is_empty() {
            return
        }

        last_query = query
        do_incremental_search(
            api: &mut owm_api
            &window
            query: query as! AK::ByteString
            owm_key
            &on_results
            &mut async
        )
    }

    timer.on_timeout = fn[&incremental_search]() {
        incremental_search()
    }

    box.on_change = fn[&incremental_search]() {
        incremental_search()
    }

    return app.exec() as! c_int
}

// FIXME: We can't call AK::String::from_byte_string() as jakt detects it as a templated function (one overload is a template, the other is not)
//        and passes a type parameter, leading to a compile error as that function is deleted.
fn ak_string<T>(anon s: T) throws -> AK::String {
    unsafe { cpp { "return AK::String::from_byte_string(s);" } }
    abort()
}