diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index c66e4da0..ad9d4e7f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -27,6 +27,9 @@
+
+
+
= listOf(PERMISSION_READ_CALENDAR)
+
+ override fun build(ctx: SkillContext): Skill<*> {
+ return CalendarSkill(CalendarInfo, Sentences.Calendar[ctx.sentencesLanguage]!!)
+ }
+}
diff --git a/app/src/main/kotlin/org/stypox/dicio/skills/calendar/CalendarOutput.kt b/app/src/main/kotlin/org/stypox/dicio/skills/calendar/CalendarOutput.kt
new file mode 100644
index 00000000..99e03a62
--- /dev/null
+++ b/app/src/main/kotlin/org/stypox/dicio/skills/calendar/CalendarOutput.kt
@@ -0,0 +1,253 @@
+package org.stypox.dicio.skills.calendar
+
+import android.content.ContentUris
+import android.content.Intent
+import android.provider.CalendarContract
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import java.time.Duration
+import java.time.LocalDate
+import java.time.LocalDateTime
+import java.time.format.DateTimeFormatter
+import java.util.Locale
+import org.dicio.skill.context.SkillContext
+import org.dicio.skill.skill.SkillOutput
+import org.stypox.dicio.R
+import org.stypox.dicio.di.SkillContextImpl
+import org.stypox.dicio.io.graphical.HeadlineSpeechSkillOutput
+import org.stypox.dicio.util.StringUtils
+import org.stypox.dicio.util.getPluralString
+import org.stypox.dicio.util.getString
+
+// TODO remind me about whatever tomorrow at nine is misinterpreted
+sealed interface CalendarOutput : SkillOutput {
+
+ data class Added(
+ private val title: String,
+ private val begin: LocalDateTime,
+ private val end: LocalDateTime
+ ) : CalendarOutput {
+ override fun getSpeechOutput(ctx: SkillContext): String {
+ val duration = Duration.between(begin, end)
+ val beginText = ctx.parserFormatter!!
+ .niceDateTime(begin)
+ .get()
+
+ return if (duration < Duration.ofHours(20)) {
+ val durationText = ctx.parserFormatter!!
+ .niceDuration(DicioNumbersDuration(duration))
+ .speech(true)
+ .get()
+ ctx.getString(R.string.skill_calendar_adding_begin_duration, title, beginText, durationText)
+ } else {
+ val endText = ctx.parserFormatter!!
+ .niceDateTime(end)
+ .get()
+ ctx.getString(R.string.skill_calendar_adding_begin_end, title, beginText, endText)
+ }
+ }
+
+ @Composable
+ override fun GraphicalOutput(ctx: SkillContext) {
+ val dateRangeFormatted = remember { formatDateTimeRange(ctx, begin, end) }
+ val duration = remember { Duration.between(begin, end) }
+ val durationFormatted = remember(duration) { formatDuration(ctx, duration) }
+
+ Column(
+ modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ Text(
+ text = ctx.getString(R.string.skill_calendar_app_was_instructed),
+ style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.Normal),
+ textAlign = TextAlign.Center
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ Text(
+ text = title,
+ style = MaterialTheme.typography.titleLarge,
+ textAlign = TextAlign.Center
+ )
+
+ Spacer(modifier = Modifier.height(3.dp))
+
+ Text(
+ text = dateRangeFormatted,
+ style = MaterialTheme.typography.bodyLarge,
+ textAlign = TextAlign.Center
+ )
+
+ if (duration >= Duration.ofSeconds(1)) {
+ Spacer(modifier = Modifier.height(2.dp))
+
+ Text(
+ text = stringResource(R.string.skill_calendar_duration, durationFormatted),
+ style = MaterialTheme.typography.bodyLarge,
+ textAlign = TextAlign.Center
+ )
+ }
+ }
+ }
+ }
+
+ data object NoCalendarApp : CalendarOutput, HeadlineSpeechSkillOutput {
+ override fun getSpeechOutput(ctx: SkillContext): String =
+ ctx.getString(R.string.skill_calendar_no_app)
+ }
+
+ data class EventsList(
+ private val events: List,
+ private val queryDate: LocalDate
+ ) : CalendarOutput {
+ override fun getSpeechOutput(ctx: SkillContext): String {
+ val formattedEvents = events
+ .take(MAX_EVENTS_TO_SPEAK)
+ .joinToString(", ") { event -> event.toSpeechString(ctx, queryDate) }
+
+ return if (events.size <= MAX_EVENTS_TO_SPEAK) {
+ ctx.getPluralString(
+ resId = R.plurals.skill_calendar_on_date_you_have_count,
+ resIdIfZero = R.string.skill_calendar_on_date_you_have_count_zero,
+ quantity = events.size,
+ formattedEvents,
+ )
+ } else {
+ ctx.getPluralString(
+ resId = R.plurals.skill_calendar_on_date_you_have_count_limited,
+ resIdIfZero = R.string.skill_calendar_on_date_you_have_count_zero,
+ quantity = events.size,
+ MAX_EVENTS_TO_SPEAK,
+ formattedEvents
+ )
+ }
+ }
+
+ @Composable
+ override fun GraphicalOutput(ctx: SkillContext) {
+ Column(
+ modifier = Modifier.fillMaxWidth().padding(top = 4.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Text(
+ text = queryDate.format(
+ DateTimeFormatter.ofPattern("MMMM d, yyyy", Locale.getDefault())
+ ),
+ style = MaterialTheme.typography.titleLarge,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxWidth(),
+ )
+
+ Spacer(modifier = Modifier.height(4.dp))
+
+ Text(
+ text = ctx.getPluralString(
+ resId = R.plurals.skill_calendar_events,
+ quantity = events.size,
+ resIdIfZero = R.string.skill_calendar_events_zero
+ ),
+ style = MaterialTheme.typography.bodyLarge,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxWidth(),
+ )
+
+ Spacer(modifier = Modifier.height(4.dp))
+
+ for (event in events) {
+ EventCard(
+ ctx = ctx,
+ event = event,
+ queryDate = queryDate,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 4.dp)
+ )
+ }
+ }
+ }
+
+ companion object {
+ const val MAX_EVENTS_TO_SPEAK = 5
+ }
+ }
+}
+
+@Composable
+private fun EventCard(
+ ctx: SkillContext,
+ event: CalendarEvent,
+ queryDate: LocalDate,
+ modifier: Modifier = Modifier
+) {
+ Card(
+ modifier = modifier,
+ onClick = {
+ if (event.id == null) {
+ return@Card
+ }
+ // open the full event description in the calendar app
+ val intent = Intent(Intent.ACTION_VIEW).apply {
+ data = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, event.id!!)
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ }
+ ctx.android.startActivity(intent)
+ },
+ elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
+ ) {
+ Text(
+ text = StringUtils.joinNonBlank(event.title, event.location)
+ .takeIf(String::isNotEmpty)
+ ?: ctx.getString(R.string.skill_calendar_no_name),
+ style = MaterialTheme.typography.titleMedium,
+ modifier = Modifier.padding(start = 11.dp, top = 8.dp, end = 11.dp, bottom = 2.dp)
+ )
+ Text(
+ text = when {
+ event.isAllDay(queryDate) -> ctx.getString(R.string.skill_calendar_all_day)
+ event.begin != null && event.end != null -> formatDateTimeRange(ctx, event.begin, event.end)
+ event.end != null -> formatDateTime(event.end)
+ event.begin != null -> formatDateTime(event.begin)
+ else -> ctx.getString(R.string.skill_calendar_unknown_time)
+ },
+ style = MaterialTheme.typography.bodySmall,
+ modifier = Modifier.padding(start = 11.dp, end = 11.dp, bottom = 8.dp)
+ )
+ }
+}
+
+@Preview
+@Composable
+private fun EventCardPreview() {
+ EventCard(
+ ctx = SkillContextImpl.newForPreviews(LocalContext.current),
+ event = CalendarEvent(
+ id = null,
+ title = "Meet with John",
+ begin = LocalDateTime.of(2026, 2, 26, 18, 0),
+ end = LocalDateTime.of(2026, 2, 26, 21, 0),
+ location = "Online",
+ isAllDay = false,
+ ),
+ queryDate = LocalDate.of(2026, 2, 26),
+ )
+}
diff --git a/app/src/main/kotlin/org/stypox/dicio/skills/calendar/CalendarSkill.kt b/app/src/main/kotlin/org/stypox/dicio/skills/calendar/CalendarSkill.kt
new file mode 100644
index 00000000..6c529e2a
--- /dev/null
+++ b/app/src/main/kotlin/org/stypox/dicio/skills/calendar/CalendarSkill.kt
@@ -0,0 +1,115 @@
+package org.stypox.dicio.skills.calendar
+
+import android.content.ActivityNotFoundException
+import android.content.Intent
+import android.provider.CalendarContract
+import android.provider.CalendarContract.Instances as CCI
+import android.util.Log
+import java.time.Duration
+import org.dicio.skill.context.SkillContext
+import org.dicio.skill.skill.SkillInfo
+import org.dicio.skill.skill.SkillOutput
+import org.dicio.skill.standard.StandardRecognizerData
+import org.dicio.skill.standard.StandardRecognizerSkill
+import org.stypox.dicio.sentences.Sentences.Calendar
+import java.time.LocalDate
+import java.time.LocalDateTime
+import java.time.LocalTime
+import org.stypox.dicio.R
+import org.stypox.dicio.util.getString
+
+
+class CalendarSkill(
+ correspondingSkillInfo: SkillInfo,
+ data: StandardRecognizerData
+) : StandardRecognizerSkill(correspondingSkillInfo, data) {
+
+ override suspend fun generateOutput(ctx: SkillContext, inputData: Calendar): SkillOutput {
+ return when (inputData) {
+ is Calendar.Create -> createEvent(ctx, inputData)
+ is Calendar.Query -> queryEvents(ctx, inputData)
+ }
+ }
+
+ private fun createEvent(ctx: SkillContext, inputData: Calendar.Create): SkillOutput {
+ // obtain capturing group data (note that we either have .end. or .duration., never both)
+ val title = inputData.title
+ ?.trim()
+ ?.replaceFirstChar { it.titlecase(ctx.locale) }
+ ?: ctx.getString(R.string.skill_calendar_no_name)
+ var begin = inputData.begin
+ ?: LocalDateTime.now()
+ var end = inputData.end
+ ?: begin.plus(inputData.duration?.toJavaDuration() ?: Duration.ZERO)
+ if (begin.isAfter(end)) {
+ val tmpBegin = begin
+ begin = end
+ end = tmpBegin
+ }
+
+ // create calendar intent
+ val calendarIntent = Intent(Intent.ACTION_INSERT).apply {
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ data = CalendarContract.Events.CONTENT_URI
+ putExtra(CalendarContract.Events.TITLE, title)
+ putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, begin.toEpochMilli())
+ putExtra(CalendarContract.EXTRA_EVENT_END_TIME, end.toEpochMilli())
+ }
+
+ // start activity
+ return try {
+ ctx.android.startActivity(calendarIntent)
+ CalendarOutput.Added(title, begin, end)
+ } catch (e: ActivityNotFoundException) {
+ Log.e(TAG, "Could not start calendar activity", e)
+ CalendarOutput.NoCalendarApp
+ }
+ }
+
+ private fun queryEvents(ctx: SkillContext, inputData: Calendar.Query): SkillOutput {
+ // we only care about the date, not the time
+ val date = inputData.`when`?.toLocalDate() ?: LocalDate.now()
+ val events = ArrayList()
+
+ ctx.android.contentResolver.query(
+ // query all events from the beginning to the end of the day
+ CCI.CONTENT_URI.buildUpon()
+ .appendPath(date.atStartOfDay().toEpochMilli().toString())
+ .appendPath(date.atTime(LocalTime.MAX).toEpochMilli().toString())
+ .build(),
+ // we want to read these fields
+ arrayOf(CCI.EVENT_ID, CCI.TITLE, CCI.BEGIN, CCI.END, CCI.EVENT_LOCATION, CCI.ALL_DAY),
+ null, // selection handled by URI
+ null, // selectionArgs handled by URI
+ "${CCI.BEGIN} ASC" // order them by begin
+ )?.use { cursor ->
+ // use ...OrThrow() because all fields surely exist as we requested them in query()
+ val eventIdIndex = cursor.getColumnIndexOrThrow(CCI.EVENT_ID)
+ val titleIndex = cursor.getColumnIndexOrThrow(CCI.TITLE)
+ val beginIndex = cursor.getColumnIndexOrThrow(CCI.BEGIN)
+ val endIndex = cursor.getColumnIndexOrThrow(CCI.END)
+ val locationIndex = cursor.getColumnIndexOrThrow(CCI.EVENT_LOCATION)
+ val allDayIndex = cursor.getColumnIndexOrThrow(CCI.ALL_DAY)
+
+ // move through all rows returned by the query and read the fields
+ while (cursor.moveToNext()) {
+ events.add(
+ CalendarEvent(
+ id = cursor.getLongOrNull(eventIdIndex),
+ title = cursor.getNonBlankStringOrNull(titleIndex),
+ begin = cursor.getDateTimeOrNull(beginIndex),
+ end = cursor.getDateTimeOrNull(endIndex),
+ location = cursor.getNonBlankStringOrNull(locationIndex),
+ isAllDay = cursor.getBooleanOrFalse(allDayIndex),
+ )
+ )
+ }
+ }
+
+ return CalendarOutput.EventsList(events, date)
+ }
+
+ companion object {
+ private const val TAG: String = "CalendarSkill"
+ }
+}
diff --git a/app/src/main/kotlin/org/stypox/dicio/skills/calendar/CalendarUtil.kt b/app/src/main/kotlin/org/stypox/dicio/skills/calendar/CalendarUtil.kt
new file mode 100644
index 00000000..763cd728
--- /dev/null
+++ b/app/src/main/kotlin/org/stypox/dicio/skills/calendar/CalendarUtil.kt
@@ -0,0 +1,59 @@
+package org.stypox.dicio.skills.calendar
+
+import android.database.Cursor
+import android.text.format.DateUtils
+import java.time.Duration
+import java.time.Instant
+import java.time.LocalDateTime
+import java.time.ZoneId
+import java.time.ZoneOffset
+import java.time.format.DateTimeFormatter
+import java.time.format.FormatStyle
+import org.dicio.skill.context.SkillContext
+
+typealias DicioNumbersDuration = org.dicio.numbers.unit.Duration
+
+internal fun LocalDateTime.toEpochMilli(): Long {
+ return atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()
+}
+
+internal fun formatDateTimeRange(ctx: SkillContext, begin: LocalDateTime, end: LocalDateTime): String {
+ return DateUtils.formatDateRange(
+ ctx.android,
+ begin.toEpochMilli(),
+ end.toEpochMilli(),
+ DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_SHOW_WEEKDAY or
+ DateUtils.FORMAT_SHOW_YEAR or DateUtils.FORMAT_SHOW_DATE or
+ DateUtils.FORMAT_ABBREV_ALL
+ )
+}
+
+internal fun formatDuration(ctx: SkillContext, duration: Duration): String {
+ return ctx.parserFormatter
+ ?.niceDuration(DicioNumbersDuration(duration))
+ ?.speech(false)
+ ?.get()
+ ?: DateUtils.formatElapsedTime(duration.toSeconds())
+}
+
+internal fun formatDateTime(date: LocalDateTime): String {
+ return date.format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG))
+}
+
+// some simple helper functions to work with Cursor...
+internal fun Cursor.getNonBlankStringOrNull(index: Int): String? {
+ return index.takeUnless(this::isNull)?.let(this::getString)?.takeUnless(String::isBlank)
+}
+
+internal fun Cursor.getLongOrNull(index: Int): Long? {
+ return index.takeUnless(this::isNull)?.let(this::getLong)
+}
+
+internal fun Cursor.getDateTimeOrNull(index: Int): LocalDateTime? {
+ val millis = getLongOrNull(index) ?: return null
+ return LocalDateTime.ofInstant(Instant.ofEpochMilli(millis), ZoneOffset.UTC)
+}
+
+internal fun Cursor.getBooleanOrFalse(index: Int): Boolean {
+ return index.takeUnless(this::isNull)?.let(this::getInt) == 1
+}
diff --git a/app/src/main/kotlin/org/stypox/dicio/util/PermissionUtils.kt b/app/src/main/kotlin/org/stypox/dicio/util/PermissionUtils.kt
index 79617da9..32cd335c 100644
--- a/app/src/main/kotlin/org/stypox/dicio/util/PermissionUtils.kt
+++ b/app/src/main/kotlin/org/stypox/dicio/util/PermissionUtils.kt
@@ -26,6 +26,10 @@ val PERMISSION_CALL_PHONE = Permission.NormalPermission(
name = R.string.perm_call_phone,
id = Manifest.permission.CALL_PHONE,
)
+val PERMISSION_READ_CALENDAR = Permission.NormalPermission(
+ name = R.string.perm_read_calendar,
+ id = Manifest.permission.READ_CALENDAR,
+)
val PERMISSION_NOTIFICATION_LISTENER = Permission.SecurePermission(
name = R.string.perm_notification_listener,
diff --git a/app/src/main/kotlin/org/stypox/dicio/util/SkillContextExt.kt b/app/src/main/kotlin/org/stypox/dicio/util/SkillContextExt.kt
index 99efd106..849521b3 100644
--- a/app/src/main/kotlin/org/stypox/dicio/util/SkillContextExt.kt
+++ b/app/src/main/kotlin/org/stypox/dicio/util/SkillContextExt.kt
@@ -1,5 +1,6 @@
package org.stypox.dicio.util
+import androidx.annotation.PluralsRes
import androidx.annotation.StringRes
import org.dicio.skill.context.SkillContext
@@ -10,3 +11,15 @@ fun SkillContext.getString(@StringRes resId: Int): String {
fun SkillContext.getString(@StringRes resId: Int, vararg formatArgs: Any?): String {
return this.android.getString(resId, *formatArgs)
}
+
+fun SkillContext.getPluralString(
+ @PluralsRes resId: Int,
+ @StringRes resIdIfZero: Int,
+ quantity: Int,
+ vararg otherFormatArgs: Any?,
+): String {
+ if (quantity == 0 && resIdIfZero != 0) {
+ return this.android.getString(resIdIfZero, quantity, *otherFormatArgs)
+ }
+ return this.android.resources.getQuantityString(resId, quantity, quantity, *otherFormatArgs)
+}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index ea8db666..b002b152 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -120,6 +120,7 @@
Do not play sound
read your contacts
directly call phone numbers
+ read your calendar
%1$s — %2$s
The skill \"%1$s\" needs these permissions to work: %2$s
Could not evaluate your request
@@ -148,6 +149,42 @@
Navigate to Vancouver international airport
Specify where you want to navigate to
Navigating to %1$s
+ Calendar
+ Add dentist appointment tomorrow at 3pm
+ Adding %1$s on %2$s lasting %3$s
+ Adding %1$s from %2$s to %3$s
+ Your calendar app was instructed to add this event:
+ Duration: %1$s
+ No calendar app found, please install a calendar app to add events
+ %1$s for all day
+ %1$s in %2$s for all day
+ in %1$s for all day
+ unnamed event for all day
+ %1$s at %2$s
+ %1$s in %2$s at %3$s
+ in %1$s at %2$s
+ unnamed event at %1$s
+ %1$s
+ %1$s in %2$s
+ in %1$s
+ unnamed event
+ You have nothing scheduled
+
+ - You have %1$d event: %2$s
+ - You have %1$d events: %2$s
+
+
+ - You have %1$d event; the first %2$d are: %3$s
+ - You have %1$d events; the first %2$d are: %3$s
+
+ No events found
+
+ - %1$d event
+ - %1$d events
+
+ All day
+ No name
+ Unknown time
Telephone
Call Tom
Timer
diff --git a/app/src/main/sentences/en/calendar.yml b/app/src/main/sentences/en/calendar.yml
new file mode 100644
index 00000000..733597d9
--- /dev/null
+++ b/app/src/main/sentences/en/calendar.yml
@@ -0,0 +1,8 @@
+create:
+ - (add|create|schedule|make|put|(arrange for?)|book|setup|(set up) ((a|an|the event|appointment|meeting called|named|(with the name)|for?)? .title. to|on|in my|the? calendar|agenda|schedule|timetable)|(a|an|the event|appointment|meeting called|named|(with the name)|for? .title.))|(schedule|(reserve a time? slot for)|(remind me about|of|to?) .title.) (starting|beginning|(that begin) on|at|for|from? the?)|(on|at|for|from the?)? .begin. and? (for|lasting|(with duration)|(that last)? .duration.)|(that is .duration. long)|(finishing|(that finish)|ending|(that end)|(lasting|(that lasts)? until)|to .end.)?
+ - block|hold|reserve|book .duration. about|of|to|for? .title. (starting|beginning|(that begin) on|at|for|from? the?)|(on|at|for|from the?) .begin.
+
+query:
+ - (what event|appointment|meeting|plan? (do|will|did i have)|(are|is|was|(will be) there?))|((do|will|did i have)|(are|is|was|(will be) there?)|(show|tell|list|read me?)? any|my|the|an? event|appointment|meeting|plan (i have)?) (on|in|inside my|the? calendar|agenda|schedule|timetable)|scheduled|arranged|happening|planned? (on|at|for? the? .when.)? (on|in|inside my|the? calendar|agenda|schedule|timetable)?
+ - (show|tell|list|read me?)|((do|will|did i have)|(is|was there? be?) anything on|in|inside) the|my? calendar|agenda|schedule|timetable (on|at|for? the? .when.)?
+ - ((am i)|(was i)|(will i be) busy|free|occupied)|(do|will|did i have free? time) (on|at|for? the? .when.)?
diff --git a/app/src/main/sentences/it/calendar.yml b/app/src/main/sentences/it/calendar.yml
new file mode 100644
index 00000000..54ff292a
--- /dev/null
+++ b/app/src/main/sentences/it/calendar.yml
@@ -0,0 +1,8 @@
+create:
+ - (aggiungi|crea|pianifica|imposta|metti|fissa|programma|calendarizza ((un|uno|una|il|lo|la|l|i|gli|le event|appuntamento|incontro|meeting chiamat|denominat|(con il nome)|(di nome)|di|del|(per|con un|uno|una|il|lo|la|l|i|gli|le?)?)? .title. a|su|ne mi? calendari|agend|diari)|(un|uno|una|il|lo|la|l|i|gli|le evento|appuntamento|incontro|meeting chiamat|denominat|(con il nome)|(di nome)|di|del|(per|con un|uno|una|il|lo|la|l|i|gli|le?)? .title.))|((riserva|blocca|tieni un|uno|una buco|spazio|(fascia oraria) per)|(ricorda di?) .title.) (che? comincia|inizia|part a?)|(a partire da)|a? .begin. e? ((di durata?)|(che dura)|(con una durata di)|lung .duration.)|((((che dura|arriva)? fino)|(che? finisc)|(con fine|finale) a?)|a .end.)
+ - riserva|blocca|tieni (un|uno|una buco|spazio|(fascia oraria) di)? .duration. di|del|(per|con il|lo|la|l|i|gli|le?) .title. (che? comincia|inizia|part a?)|(a partire da)|a? .begin.
+
+query:
+ - (che|qual event|appuntament|incontr|meeting|pian|programm (ho avuto?)|avro|avevo|(c (sono|e stat)|sara|era))|(mostra|di|leggi|(ho avuto?)|avro|avevo|(c (sono|e stat)|sara|era) un|uno|una|il|lo|la|l|i|gli|le|de event|appuntament|incontr|meeting|pian|programm) programmat|(in programma)? (su|ne mi? calendari|agend|diari)? ((per (il giorno?)?)|il|a .when.)? (su|ne mi? calendari|agend|diari)?
+ - mostra|di|leggi|((ho avuto?)|avro|avevo|(c (sono|e stat)|sara|era) qualcosa) su|ne mi? calendari|agend|diari ((per (il giorno?)?)|il|a .when.)?
+ - (sono|saro|ero occupato|preso|libero)|(ho|avevo|avro tempo libero|(a disposizione)?)((per? (il giorno?)?)|il|a .when.)?
diff --git a/app/src/main/sentences/skill_definitions.yml b/app/src/main/sentences/skill_definitions.yml
index ed347e96..7d1b12e8 100644
--- a/app/src/main/sentences/skill_definitions.yml
+++ b/app/src/main/sentences/skill_definitions.yml
@@ -134,3 +134,21 @@ skills:
type: string
- id: target
type: string
+
+ - id: calendar
+ specificity: high
+ sentences:
+ - id: create
+ captures:
+ - id: title
+ type: string
+ - id: begin
+ type: date_time
+ - id: end
+ type: date_time
+ - id: duration
+ type: duration
+ - id: query
+ captures:
+ - id: when
+ type: date_time