From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from mp12.migadu.com ([2001:41d0:8:6d80::]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)) by ms5.migadu.com with LMTPS id sAqEB/EIs2KjfQAAbAwnHQ (envelope-from ) for ; Wed, 22 Jun 2022 14:20:01 +0200 Received: from aspmx1.migadu.com ([2001:41d0:8:6d80::]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)) by mp12.migadu.com with LMTPS id oERfB/EIs2LeawAAauVa8A (envelope-from ) for ; Wed, 22 Jun 2022 14:20:01 +0200 Received: from lists.gnu.org (lists.gnu.org [209.51.188.17]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by aspmx1.migadu.com (Postfix) with ESMTPS id 3361B2DFF7 for ; Wed, 22 Jun 2022 14:20:00 +0200 (CEST) Received: from localhost ([::1]:49248 helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1o3zKw-0006EZ-Vv for larch@yhetil.org; Wed, 22 Jun 2022 08:19:59 -0400 Received: from eggs.gnu.org ([2001:470:142:3::10]:43408) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1o3zK5-0005vi-Tr for emacs-orgmode@gnu.org; Wed, 22 Jun 2022 08:19:06 -0400 Received: from mail-he1eur04olkn080d.outbound.protection.outlook.com ([2a01:111:f400:fe0d::80d]:9318 helo=EUR04-HE1-obe.outbound.protection.outlook.com) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1o3zJx-00053u-P8 for emacs-orgmode@gnu.org; Wed, 22 Jun 2022 08:19:05 -0400 ARC-Seal: i=1; a=rsa-sha256; s=arcselector9901; d=microsoft.com; cv=none; b=V/fXWkxLdjmywiLzJaH7OIPHq+4Xg+/Mf/b/SdbyLqe7vy29NbbiKXHQP5nryIB0RCkksH/kUMpnzMtvuP7WCR2jBIX84G8emvID+9JGM0mYpBnRIzrD3cY4jYNbEC2naKEZ0XFFJYaDkBKhz69QPJ7+EI+k3p244luszp64T1jGMrbs6RN+BvLmjQt80bCBPh5pH+tlXD+GWFeNxjHw1DqYXkEUDZ6J0DPYPFRJpBB7VkSiGBZ2EU2RQP0/StW/YqAgjUTppLEZ4nBVSo3yjnWBJNfa4TPxVSkoVABs8o6eam+687cImUoCfgugh4eDksAd+Jk1I9XzeP91+duDAQ== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=microsoft.com; s=arcselector9901; h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-AntiSpam-MessageData-ChunkCount:X-MS-Exchange-AntiSpam-MessageData-0:X-MS-Exchange-AntiSpam-MessageData-1; bh=YaZlXk0Tw3rwyHB7FA7KFArJTp9aTSLXjfuTbA2eQTk=; b=fddMa+7W0f2LKi9G0343MRO2XrtbrP6aiFdfyrGxEM+WGs/F4aeRaKncdKGBdvxYJH7kdmWQFqSmH84dTOPbP4mnTNPBNWJ28FGGruWHbYVxaTsY0NPmqAsDKCkfeLK3JJqhgq5F3g3jvPnmueI7pv8JSWhqtw3MMJWtE8c23Bz6GMt3M2cdyZDCDJm+6sKfuXqucxTR7TRVSTJJI6yVd4Hv60XaDwqxvBsfxwQNkV7fO/kBJk6SJzJzZoFGOwwbXeuGQm/JY9bLCRV269fx2AJECxmXc1zE/mWJHqOpjm4c70L/kkl4E4BhRJ5I7u9ijoGF5tRlFpeplrPSdjld8A== ARC-Authentication-Results: i=1; mx.microsoft.com 1; spf=none; dmarc=none; dkim=none; arc=none DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=live.com; s=selector1; h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-SenderADCheck; bh=YaZlXk0Tw3rwyHB7FA7KFArJTp9aTSLXjfuTbA2eQTk=; b=rOVmsTjkD7FRpZCNYb/vYwcHm3fZSXwluP8T7IzYfP8mGhQ/VvzQlkarUCDcCW23icD8JD71GgEjC3Zy7Nbfnh5/WnqAe/CdyQmi/fTONXvjCpJwzLRFuakxOk9ySYvV0iMFvO7aadSBJs1PJOP2eH6Ouy10oygj0T5pIMYavPjNZ+zCm/XN2ASz0oU1Uwgb/0IsYPQ12VF7MD9H/mlEyaLWZ0DtrOf1NZbjZR4wClDtHuEXjxAyV0GBAp3rXF6PYZmeM8f/hs4Q60KslTDoMEJBCQoF8SttL13hOOfhBDclBf0UhJyadufR44FoLnprpVzO4JPMiQs5qCuj0TOwfg== Received: from AM9PR09MB4977.eurprd09.prod.outlook.com (2603:10a6:20b:304::20) by AM0PR09MB4387.eurprd09.prod.outlook.com (2603:10a6:20b:165::12) with Microsoft SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.20.5353.15; Wed, 22 Jun 2022 12:13:52 +0000 Received: from AM9PR09MB4977.eurprd09.prod.outlook.com ([fe80::1995:84ad:afa1:1f39]) by AM9PR09MB4977.eurprd09.prod.outlook.com ([fe80::1995:84ad:afa1:1f39%7]) with mapi id 15.20.5373.015; Wed, 22 Jun 2022 12:13:52 +0000 From: Arthur Miller To: Max Nikulin Cc: emacs-orgmode@gnu.org, Ihor Radchenko Subject: Re: Proposal: 'executable' org-capture-templaes References: <87pmjyco0x.fsf@localhost> <87fskrobiw.fsf@localhost> <87a6ay1enh.fsf@localhost> <87edzvdb44.fsf@localhost> <878rpu5qf4.fsf@localhost> <87zgi7357y.fsf@localhost> <875ykuslpx.fsf@localhost> Date: Wed, 22 Jun 2022 14:13:51 +0200 In-Reply-To: (Max Nikulin's message of "Tue, 21 Jun 2022 22:48:18 +0700") Message-ID: User-Agent: Gnus/5.13 (Gnus v5.13) Emacs/29.0.50 (gnu/linux) Content-Type: multipart/mixed; boundary="=-=-=" X-TMN: [EvYCI06JQIc2DBI5esdGjKElSo3dQTEC] X-ClientProxiedBy: CWLP265CA0285.GBRP265.PROD.OUTLOOK.COM (2603:10a6:401:5c::33) To AM9PR09MB4977.eurprd09.prod.outlook.com (2603:10a6:20b:304::20) X-Microsoft-Original-Message-ID: <87v8ssq4jk.fsf@live.com> MIME-Version: 1.0 X-MS-Exchange-MessageSentRepresentingType: 1 X-MS-PublicTrafficType: Email X-MS-Office365-Filtering-Correlation-Id: f96f1afc-ca68-480d-674f-08da5448abbc X-MS-TrafficTypeDiagnostic: AM0PR09MB4387:EE_ X-Microsoft-Antispam: BCL:0; X-Microsoft-Antispam-Message-Info: +J6E875rPw6ZKl/ldz8beWN+JKzd7IYJ069DoV/1X2RWaa9eLLqvRiJC0w+taMEuuQKxj/bsemJPOa7UODCPZn+NYnBAsUyJ/ZC8ySKRJe2Mk32kQDpEjjSKWVnzkntzCqsVvc3ua6Hl1H/sLPswAJKuj2FJI8g6N/1kAGfARZ0zwkn13ttON1nh+jEOU0MhXBIKMAi8DRva50Fbw+R4tqkZAe6RqFOL7ZqOrpgU4npdzcYJTgilwzTDlqw1N/oo5T4m2P0cd40+0tukif3yGAvsT0KIW1fAFZWV83H36ItARNfAqCdPVa6MSeoH2CRK2VY9fckAn6We2QJncD8+QL4SvtuFmPP+LTisLX6f0T77G2Pb2hXVySvB/zlRCgA7CT283FubOI+axOJPk4n7+XdLhfEnLilwliC3Xwwjl6uSIqWqvceMNiEjJudHN0DlQQie7yvDtjVU96tI3YKQy9cPtc3WUeavK8A9b/dgXwpuscsfAF/TaL5/HfQBkgZYz7ZaXP52W14U5Vg9LNiK/8bI/YGoGVQONlJ0NSJVmHDVPcT3eZxQtiVU4roZ3N/D9w90Xe1PyjPrHvrU/oyzLkfMWvfpgRHzF7OMAKWJZ0ltTPOV1ijLaKzI5zs3nOVP X-MS-Exchange-AntiSpam-MessageData-ChunkCount: 1 X-MS-Exchange-AntiSpam-MessageData-0: =?us-ascii?Q?A+L/hj1+N0D5qg5xEp4WWPKq2jUXP0mNzlE9F4Ua0Aeu0VgyupDP5P4/5VZu?= =?us-ascii?Q?ADdkM2tlfyfXwvQ5r6tJUjEJynJguC9U4UggeYphhedzFXg1ldowABAVVEwd?= =?us-ascii?Q?ChtG9k5WtHOe9YLyA0ve3r/SfbavbJetGyCKmK9Cxj21ytghox31gaTo5SDK?= =?us-ascii?Q?pgWqSsBImN5Jc19DNe49SNOZM46VrK2Q7Kgpb8PMzvKEsuZfxGsnF6FZHi1b?= =?us-ascii?Q?uTmOKZihjuliuXTYbE991HcY5mtCVPIPuwc4alAPzb4rDeYLaPVcwoc+yxzB?= =?us-ascii?Q?u5GUpQcRi4JyvW08nSHOitniVxfYCCWKMcT+R3prruhUr7xIoVZ9OHQm8uuX?= =?us-ascii?Q?DbooEoS2JPzzKyGVwSHiwoO6cwSk7Cipg0pmI9Y327ychqywCgoF5hQAnctz?= =?us-ascii?Q?0FPO0zrTUgFM81xSsnVFfQqmRj+XQdtz3VRZMIo4CKDTuk5ILZGt4j1IE81z?= =?us-ascii?Q?YdujS8eyPkAIs6xpEg3jenHeccPAVduOMvHuNnmWYdf+kFXyxcz2/dkhNJAH?= =?us-ascii?Q?cAkQi+0lWt0sOwYXuj+/hD6yXeWJnBWyxKfw+fB43mwAPvzZkXkk5Zl29MZp?= =?us-ascii?Q?d0Jc4cXiFvLLNBWBIH0Z2sUigsPFoMVsYXwl2uoZHFG8jR5J+P2i/kBVWVh3?= =?us-ascii?Q?rxmqThekkUn3fFIJ9nvFjaVfp7RhuB2/skO2kdnACTExAME1Re3F3wE/SnZ+?= =?us-ascii?Q?241WSYSmjQnHPAhsiJeJCnYhLzVMWlbMrkb7dxpZ0HNodJ1Zmo30J59psn3l?= =?us-ascii?Q?1mmRc4YltZ3kYcDw1ujvMSyClvp6uLaUeE748b9BsmXR0j7wvSQyHSjOaQEs?= =?us-ascii?Q?k0F9kUlevqq0RtjQf5iVZNcq/MH+ytrP/7n1RYd9WAQuxJuq86+8dfw8kMab?= =?us-ascii?Q?62ArzarKQpzIQKuLer9v5qj2KST1KEi8oliFUHKRW8p5FF+5Py/L11lPV4gy?= =?us-ascii?Q?h8TU1oKyuUR3eOd8rPaD7xh7gmhZ2XqCmsXfRxWaodKYV0U0HI8bySloIK7Z?= =?us-ascii?Q?qSVSed29Fd+n5GC/WgWW9CkKQ8vqPlAAN6IqvsRR4NSsOyu5vx5laU9aauQ2?= =?us-ascii?Q?LqPxNQYNkexYCMnZZx66qeSiRkiqrWM0SDzJNVV83cIXB9CNAJNTufJI5Tlg?= =?us-ascii?Q?q/gg3uaZmv0cySbx0Ehh2DkX+Nwlb8hTOW6TyDctkii/ccV7W+yAbDq9USUa?= =?us-ascii?Q?klQ8aQWzk7BcTIusXKbCVOm63dzLlx3N9uQD5+HnIf00KK6pusV8F+GSqiQ8?= =?us-ascii?Q?myuwMnggQzRI/L1dLNn+gLZlWfHy7AyUzezpCaQ5s4Ql0/vHDNdoc/2rlfDE?= =?us-ascii?Q?nvL/j8PF6nMMIw0iOGGir5SJSPkLLGc1CvAqlhDZ/DE9FTA7xgyVHuK95VuT?= =?us-ascii?Q?DG2eoTtTfQbrLU72FlkOtjv5EqW7lTg1AKHoo4sgIYtAm8yCwWqDTOQdYfcQ?= =?us-ascii?Q?hh37YquMfIg=3D?= X-OriginatorOrg: sct-15-20-4755-11-msonline-outlook-64da6.templateTenant X-MS-Exchange-CrossTenant-Network-Message-Id: f96f1afc-ca68-480d-674f-08da5448abbc X-MS-Exchange-CrossTenant-AuthSource: AM9PR09MB4977.eurprd09.prod.outlook.com X-MS-Exchange-CrossTenant-AuthAs: Internal X-MS-Exchange-CrossTenant-OriginalArrivalTime: 22 Jun 2022 12:13:52.7132 (UTC) X-MS-Exchange-CrossTenant-FromEntityHeader: Hosted X-MS-Exchange-CrossTenant-Id: 84df9e7f-e9f6-40af-b435-aaaaaaaaaaaa X-MS-Exchange-CrossTenant-RMS-PersistedConsumerOrg: 00000000-0000-0000-0000-000000000000 X-MS-Exchange-Transport-CrossTenantHeadersStamped: AM0PR09MB4387 Received-SPF: pass client-ip=2a01:111:f400:fe0d::80d; envelope-from=arthur.miller@live.com; helo=EUR04-HE1-obe.outbound.protection.outlook.com X-Spam_score_int: -20 X-Spam_score: -2.1 X-Spam_bar: -- X-Spam_report: (-2.1 / 5.0 requ) BAYES_00=-1.9, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, FREEMAIL_FROM=0.001, SPF_PASS=-0.001, T_SCC_BODY_TEXT_LINE=-0.01, T_SPF_HELO_TEMPERROR=0.01 autolearn=ham autolearn_force=no X-Spam_action: no action X-BeenThere: emacs-orgmode@gnu.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: "General discussions about Org-mode." List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: emacs-orgmode-bounces+larch=yhetil.org@gnu.org Sender: "Emacs-orgmode" X-Migadu-Flow: FLOW_IN X-Migadu-To: larch@yhetil.org X-Migadu-Country: US ARC-Message-Signature: i=2; a=rsa-sha256; c=relaxed/relaxed; d=yhetil.org; s=key1; t=1655900400; h=from:from:sender:sender:reply-to:subject:subject:date:date: message-id:message-id:to:to:cc:cc:mime-version:mime-version: content-type:content-type:in-reply-to:in-reply-to: references:references:list-id:list-help:list-unsubscribe: list-subscribe:list-post:dkim-signature; bh=YaZlXk0Tw3rwyHB7FA7KFArJTp9aTSLXjfuTbA2eQTk=; b=EvEfkAVAykXSW9SNaN0AiJdC4U4zu/InKd3kNVvEA5upIe5D3D5B73IgdAiW4Cb6ougNjb RyHA5Bt6TfgZd4fspweOBktu86//goozCYxmCNpLFtBI4V/wviiYDg34JARACeKA8E8g+0 ECBolke6eYd/+mnrjYCarCr3M9cS98h5rCHqELPas5kLV6LWHJYiqZ7od2jSgoJQludxRA SyXnWCQvLwZhCgqfVOlLwmxSYVKiQ1i+w1audgsRo6kUERmgrsRRsDY1v6bnZYxnlnQX5C asjAHWCvB038FGFLDj//EWi/ezFIkqwANmqXxpb+16YXF5rPn1+eU8mc9XL+Rw== ARC-Seal: i=2; s=key1; d=yhetil.org; t=1655900400; a=rsa-sha256; cv=pass; b=pomjzULsa09svU+LOW8FhWcbPqGoiGuZlfOwSpRYU8mNkR3vFW6O6XAa6P0pPItftyhYsQ TURa9NT68MpLAW2WBz/MgE2W+YgUxB0oVF8DDUOl3gJyBBalrXpddKpcNSD2ubBOhK5rlc a7N1IBZS0yTSPBouZoyNPbAWrbBu822JX3VgBnfIes5pmGd5GJkXYQoxsCtxvdHQMpws0Y 2QyEQy7TVmsZUO/888YL6VQO6h6IVmraQefKeZuerWu31uYzBQuct7lNzU6JYsLQBjpuym Q8mT9KYvE19sD7ewFEwcIpSAGkomWcIbMwwUxM8NhmA3SNwlUaTjeRJzlmJudw== ARC-Authentication-Results: i=2; aspmx1.migadu.com; dkim=pass header.d=live.com header.s=selector1 header.b=rOVmsTjk; arc=pass ("microsoft.com:s=arcselector9901:i=1"); dmarc=pass (policy=none) header.from=live.com; spf=pass (aspmx1.migadu.com: domain of "emacs-orgmode-bounces+larch=yhetil.org@gnu.org" designates 209.51.188.17 as permitted sender) smtp.mailfrom="emacs-orgmode-bounces+larch=yhetil.org@gnu.org" X-Migadu-Spam-Score: -6.46 Authentication-Results: aspmx1.migadu.com; dkim=pass header.d=live.com header.s=selector1 header.b=rOVmsTjk; arc=pass ("microsoft.com:s=arcselector9901:i=1"); dmarc=pass (policy=none) header.from=live.com; spf=pass (aspmx1.migadu.com: domain of "emacs-orgmode-bounces+larch=yhetil.org@gnu.org" designates 209.51.188.17 as permitted sender) smtp.mailfrom="emacs-orgmode-bounces+larch=yhetil.org@gnu.org" X-Migadu-Queue-Id: 3361B2DFF7 X-Spam-Score: -6.46 X-Migadu-Scanner: scn0.migadu.com X-TUID: i+TMDR0RUdvU --=-=-= Content-Type: text/plain Max Nikulin writes: > On 21/06/2022 11:07, Ihor Radchenko wrote: >> Max Nikulin writes: >> The other question is altering the org-capture code. >> I think that it is too early to discuss org-capture part just yet. >> Lets first finalize the generic library itself and discuss the >> appropriate adjustments to >> org-capture/org-agenda/org-export/org-todo/etc code later. > > From my point of view changing menu without adjusting calling logic is a kind of > trade-off with uncertain net result. New UI may significantly affect different > user expectations. It seems this is the point of our disagreement. You tend to > consider menu in isolation trying to postpone the question concerning > interaction of code running before menu creation and handlers of menu items > (they are not belong to the same call stack any more). > > What I am really afraid is words like: > >> Sure. That somewhere can be buffer-local variable inside the capture >> menu buffer. Or global variable. Or closure. How is it relevant to the >> capture menu? Yepp, it can't saved in a buffer-local before the buffer is created. > I am trying to avoid implementation of menu that would make passing state to > handlers unreliable or prohibitively complicated. Menu should and application should be separated in my eyes. Menu is just a graphical/audio? presentation of selectable choice to the user. As such it should display choices, let user pick a choice, and return to the application the picked choice. Compare to completing-read, or org-mks. I don't think we should mingle application states and menu states. However, there is a slight complication compared to completing-read or org-mks in regard that those are used to make only one selection and exit, while this menu is supposed to be used that way, or to be used in a way that it survives multiple selections. To me this seems as clearly per menu entry behaviour. For exmaple in org-agenda menu, some actions will make menu exit, while some will keep the menu alive (for example '*'). One solution is to have some flag passed per entry to signal to the menu to kill itself upon selection. The other one, the easier, is to let the applicaiton code handle the menu kill. I took the second one to minimize on number of flags that needs to be passed and documented, but I agree it is not the cleanest way, so I can change to use a flag if it is considered a better option. > Global variables are certainly not an option because such approach does not > allow several instances of menu created in parallel. Yes, please, lets try to minimize number of globals. Actually I would be happiest with a functional approach where everything is communicated via function arguments and return values. > To use buffer-local variables some support from the side of menu library is > required. Buffer does not exist when menu function is called. So either some > callback should be passed to menu to be invoked when a new buffer is created for > menu or that buffer should be returned from the menu creating function. Unsure > if (current-buffer) would be a reliable approach. > > Closures should work, but would be it convenient enough? We are using list to pass both menus and menu entries. Application can stick in some args used by the menu. > Maybe some intermediate layer should be introduced to make alternative > implementation of menu (transient, completing read, etc.) easier. I don't think it is the topic and scope of this menu. It would be another layer on its own, and definitely another project, I think. > I believe, discussing design of menu without evaluation if it is suitable to > features that should use it (capture, agenda, etc.), there is a risk to create > unreasonable restrictions on further steps. I definitely agree. Designing a good API is hard. I personally always think that frameworks should not be designed in advance. I am more of a pragmatic, and prefer to see actual working use-cases and design a framework after the experience, not try to desing a framework by guessing the potential use-cases. That is why I said that I am not interested in turning this into a framework. I have try to design here something that can be used to implement existing org-capture, org-agenda & co on top of it. > That is why I consider the following aspects of menu design as essential ones: > - A clearly determined way to pass some state from a function that requests > creation of menu to handlers of menu items. Menu entries (lists) > - When a non-blocking technique to wait user decision is used, multiple > instances of the same menu should be allowed. Still have to be worked on. > - Defined extension points for alternative implementations to avoid code > duplication and divergence of behavior. That would be some framework layer. > My fear is that if such points are ignored in the beginning, it may become > impossible later. It's lisp, anything is possible ^^ ;-). No, but serioiusly, there is not much of extension points here. It is a keymap and buffer text generated automatically from a list of templates and few "visual" options. An application can even discard entire buffer and draw its own text out of templates as it pleases, it wouldn't matter. My goal was to create something very simple to use from a programming perspective. Compared to other similar options like make-help-screen and org-mks this puts both key handling, actions and labels into same spot, so there is only one place to maintain later on, and client code needs needs just to specify the most minimal lisp one is interested to execute, ie. I don't need to create lambdas if some function is not already defined. Sort of, that is how at least I would like to use it. I don't know if that is good or bad practice, or how robust the code is, I am not super familiar with lisp and org-mode to be honest. Attached is another experiment. In this version each entry can have attached handler, and I provide two simple handers: run-once and run-multiple times. For applications that need more advanced handling, they are supposed to provide own handler and to take care of killing the menu buffer. I do have thoughts to rework the drawing code once more to allow entries in form of: (:separator sepeartor-string), similar as it takes group entries for submenus. It would allow for mixing horizontal and vertical layouts, but I am not sure if that is a good feature to have. It certainly isn't needed. I have reworked a bit org-capture, but I haven't yet worked on org-agenda restrictions and other details. --=-=-= Content-Type: text/plain; charset=utf-8 Content-Disposition: attachment; filename=org-select.el Content-Transfer-Encoding: quoted-printable ;;; org-select.el --- Build custom menus from declarative templates -*- le= xical-binding: t; -*- ;; Copyright (C) 2022 Arthur Miller ;; Author: Arthur Miller ;; Keywords: tools ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see . ;;; Commentary: ;; Org-select is a selection framework meant to be simple and easy to use. = Its ;; job is to display a list of choices for a user to pick from and to hand = in ;; the selection to the client application. Org-select can be used to allow ;; either a single selection at a time, or for repeated selections from a ;; menu-like text-buffer.=20 ;; One of goals with this framework is to be easy to setup for the client ;; code. For that reason, org-select uses simple template language modeled = after ;; org-capture templates. ;;; Code: =0C (require 'org-macs) ;;; User vars =0C (defgroup org-select nil "Create menus from declarative templates." :prefix "org-select-" :prefix "osl--" :tag "Org Select" :group 'org) (defcustom org-select-back-key [f10] "Used to render string for the horizontal separator." :type 'character :group 'org-select) (defcustom org-select-horizontal-separator "|" "Used to render string for the horizontal separator." :type 'string :group 'org-select) (defcustom org-select-vertical-separator "-" "Used to render string for the vetical separator." :type 'string :group 'org-select) (defcustom org-select-key-decorator-chars "" "Characters used to decorate shortcut keys. This string should contain only two characters, the first one for the left decorator and the second one for the right decorator. Example: string \"[]\" will render key \"C\" as \"[C]\"." :type 'string :group 'org-select) (defcustom org-select-label-decorators (cons "..." "...") "Used to render string for the vetical separator." :type 'cons :group 'org-select) =0C ;;; Implementation =0C (defvar-local osl--init nil) (defvar-local osl--args nil) (defvar-local osl--buffer nil) (defvar-local osl--menu-begin nil) (defvar-local osl--buffer-menu nil) (defvar-local osl--longest-label 0) (defvar-local osl--buffer-window nil) (defvar-local org-select-mode-map nil) (defvar-local osl--horizontal-layout nil) (defvar-local osl--current-menu-column nil) (defvar-local osl--handler-fn nil "The handler invoked when per-menu handler is not specified. The default one is org-select-run-once.") (define-minor-mode org-select-mode "" :interactive nil :global nil) ;;;; Help-functions (defun osl--arg (key) (plist-get osl--args key)) (defun osl--init () (buffer-local-value 'osl--init (current-buffer))) (defun osl--prop (property list) "Return value of PROPERTY from irregular plist LIST." (cadr (member property list))) (defun osl--ignore-key () (interactive) (message "Invalid key %S" ;; I am not happy but it works somewhat (edmacro-format-keys (vector last-input-event)))) (defun org-select-quit (&optional abort-message buffer-or-name) "Callback to quit an org-select buffer. If given, and optional ABORT-MESSAGE will be printed instead of the default one. BUFFER-NAME can be used to quit org-select mode from a non org-select buffer." (interactive) (let ((window (if buffer-or-name (get-buffer-window buffer-or-name) osl--buffer-window))) (when (window-live-p window) (select-window window) (quit-window (buffer-live-p buffer-or-name) window)) (message (or abort-message "Org Select Quit")))) (defun osl--make-mode-map () (let ((map (make-sparse-keymap))) (define-key map [?q] #'org-select-quit) (define-key map [?\C-g] #'org-select-abort) (define-key map [?\C-p] #'osl--back) (define-key map [remap newline] #'osl--ignore-key) (define-key map [remap self-insert-command] #'osl--ignore-key) (setq org-select-mode-map map) (use-local-map org-select-mode-map))) (defun org-select-abort () (interactive) (org-select-quit "Aborted")) (defun osl--back () (interactive) (when (bound-and-true-p org-select-mode) (osl--make-mode-map) (osl--draw))) (defun osl--longest-line () "Return the length of the longest line in current buffer." (let ((n 1) (L 0) (e 0) (E (point-max)) l) (while (< e E) (setq e (line-end-position n) l (- e (line-beginning-position n)) n (1+ n)) (if (> l L) (setq L l))) L)) (defun osl--decorate-key (key) "Place string KEY between characters specified in DECORATOR string." (let ((kd (if (> (length org-select-key-decorator-chars) 0) org-select-key-decorator-chars (osl--arg :key-decorator)))) (if (=3D (length kd) 2) (concat (substring kd 0 1) key (substring kd 1)) key))) (defun osl--decorate-label (entry) (let ((left (car org-select-label-decorators)) (right (cdr org-select-label-decorators))) (if (=3D (length entry) 2) (concat left (cadr entry) right) (cadr entry)))) (defun osl--make-separator (&optional marker length) (let ((len (or length (osl--longest-line))) (sep (if (osl--arg :horizontal) org-select-horizontal-separator org-select-vertical-separator))) (if marker (concat "sep" sep) (make-string len (string-to-char sep))))) (defun osl--insert-horizontal-separator (sep) (goto-char 1) (let ((lol (osl--longest-line)) (sep (or org-select-horizontal-separator sep))) (while (not (eobp)) (let* ((eol (line-end-position)) (bol (line-beginning-position)) (fill (- (+ bol lol) eol))) (goto-char eol) (if (> fill 0) (while (> fill 0) (insert " ") (setq fill (1- fill))) (while (> 0 fill) (delete-char 1) (setq fill (1+ fill)))) (insert " " sep " ")) (forward-line)) (setq osl--current-menu-column (+ lol (length sep) 2)))) (defun osl--insert-separator (sep) (if (osl--arg :horizontal) (osl--insert-horizontal-separator sep) (insert sep))) (defun osl--insert (&rest strings) (cond ((and (osl--arg :horizontal) (> osl--current-menu-column 0)) (goto-char (+ (line-beginning-position) osl--current-menu-column)) (apply #'insert strings) (if (char-after) (forward-line) (insert "\n"))) (t=20 (apply #'insert strings) (insert "\n")))) (defun osl--forward-menu () (cond ((osl--arg :horizontal) (goto-char (point-min)) (goto-char (line-end-position)) (setq osl--current-menu-column (- (point) (line-beginning-position)))) (t (insert "\n")))) ;;;; Menu drawing (defun osl--setup-buffer (tables args) "Setup buffer local variables needed for an org-select buffer." (let* ((buffer (or (plist-get args :buffer-name) "*Org-select: ")) (window (get-buffer-window buffer))) (if window (select-window window) (org-switch-to-buffer-other-window buffer)) (with-current-buffer (get-buffer buffer) (special-mode) (setq cursor-type nil) (org-select-mode) (osl--make-mode-map) (setq osl--args args osl--buffer-menu tables osl--current-menu-column 0 osl--buffer (current-buffer) osl--buffer-window (get-buffer-window))))) (defun osl--draw () "Starts menu parsing." (with-silent-modifications (erase-buffer) (setq osl--init nil) (let ((marker (osl--make-separator 'marker)) (text (osl--arg :text)) (menus (buffer-local-value 'osl--buffer-menu (current-buffer)))) (setq osl--menu-begin (point)) ;; given a list of menus, display one menu at a time (dolist (menu menus) (cond ((symbolp menu) (setq menu (eval menu))) ((symbolp (car menu)) (setq menu (eval (car menu))))) (let ((handler (osl--prop :org-select-handler menu))) (when handler (setq menu (delete :org-select-handler (delete handler menu)))) (osl--do-menu menu (or handler #'org-select-run-once))) (setq menus (cdr menus)) (when menus (osl--insert-separator marker) (osl--forward-menu))) ;; redraw markers with real separator strings (goto-char 1) (let ((sep (osl--make-separator nil (osl--longest-line)))) (while (search-forward marker nil t) (replace-match "") (osl--insert-separator sep))) ;; insert info text if any (when text (goto-char 1) (insert "\n" text "\n")) (org-fit-window-to-buffer) (setq osl--init t) (goto-char 1)))) ; unnecessary but prettier if beacon-mode is active ;; iterate through menu and render a single entry or a group of entries on = each ;; iteration (defun osl--do-menu (menu handler) "Insert one menu at a time." (while menu (let ((entry (car menu))) (setq menu (if (> (length entry) 2) (osl--do-entry menu handler) (osl--do-group menu handler)))))) (defun osl--do-group (menu handler) "Do a menu with group nodes." (let ((group (car menu)) newmenu) (osl--do-entry menu handler) (while (> (length (cadr menu)) 2) (let (entry newentry key) (setq menu (cdr menu) entry (car menu)) (setq key (substring (car entry) 1)) (push key newentry) (dolist (elt (cdr entry)) (push elt newentry)) (push (nreverse newentry) newmenu))) (setq newmenu (nreverse newmenu)) (define-key org-select-mode-map (kbd (car group)) (lambda () (interactive) (with-silent-modifications (erase-buffer) (setq osl--current-menu-column 0) (osl--do-menu newmenu handler)))) (cdr menu))) ;; return next group in chain ;; we send in the entire menu so we can return next piece in chain, ;; but *the* entry we work with is just the very first one (car menu) (defun osl--do-entry (menu handler) "Display a single entry in the buffer." (let* ((entry (car menu)) (key (car entry)) (line-length 0) (handler (or (osl--prop :org-select-handler entry) handler))) (define-key org-select-mode-map (kbd key) (lambda () (interactive) (let ((label (nth 1 entry)) (init (buffer-local-value 'osl--init osl--buffer))) (and init handler (message (or (funcall handler entry (current-buffe= r)) label)))))) (osl--insert (osl--decorate-key key) " " (osl--decorate-label entry)= ) (setq line-length (- (line-end-position) (line-beginning-position))) (if (> line-length osl--longest-label) (setq osl--longest-label line-length)) (cdr menu))) (defun org-select-run (entry &optional _org-select-buffer) "Try to execute form found in ENTRY if any leave ORG-SELECT-BUFFER live. This handler provides an easy way to use the framework for the simple use-cases for multiple choices. It relies on the user to press built-in cho= ice `q' or `C-g' to exit the menu." (let* ((form (nth 2 entry)) (message (cond ((commandp form) (call-interactively form)) ((functionp form) (apply form (cddr entry))) (t (eval form))))) (if (stringp message) message))) (defun org-select-run-once (entry &optional org-select-buffer) "Try to execute form found in ENTRY if any and kill ORG-SELECT-BUFFER. Provides an easy way to use the framework for the simple use-cases for mult= iple choices. It relies on the user to press built-in choice `q' or `C-g' to exi= t the menu." (if org-select-buffer (org-select-quit "")) (let* ((form (nth 2 entry)) (message (cond ((commandp form) (call-interactively form)) ((functionp form) (apply form (cddr entry))) (t (eval form))))) (if (stringp message) message))) =0C ;;; API =0C (defun org-select (menus &rest args) "Select a member of an alist with multiple keys. MENUS is a list of menus which themselves are lists containing entries in o= ne of following two formats: 1. prefix descriptions like (\"a\" \"Description\") This indicates that `a' is a prefix key for multi-letter selection, and that there are entries following with keys like \"ab\", \"ax\"... 2. Select-able members must have more than two elements, with the first being the string of keys that lead to selecting it, and the second a short description string of the item.=20 The command will then make a temporary buffer listing all entries that can be selected with a single key, and all the single key prefixes. When you press the key for a single-letter entry, it is selected= . When you press a prefix key, the commands (and maybe further prefixes) under this key will be shown and offered for selection. Each menu can be followed by some properties in form of a keu-value pair. T= he entire menu or entry does not need to be a regular plist. Following keys ar= e recognized: :org-select-pin Pin this menu in org-select buffer. If group nodes are = used, when this option is `t', keep this menu visible even wh= en descending into a submenu. ;; FIXME Not implemented yet= . :org-select-handler Use this function to handle this particular menu or entry. When none is specified, org-select uses `org-select-run-once' to hande the menu. Entry handler takes precedence over menu handler. If there are more than one menus, they will be separated by a separator lin= e rendered with character as specified in `org-select-horizontal-separator'. ARGS is a org-select buffer or entry property list containing following mem= bers: :text a string placed over selections in the buffer. :buffer-name a string used for the selections buffer name. :key-decorator a two-character string used to decorate command characters. = A menu can specify this string, but the precedence will be giv= en the global variable `org-select-key-decorator-chars'. This t= o ensure that users can customize the appearance of the menus. Properties in ARGS list are global for the entire org-select buffer." =20 (osl--setup-buffer menus args) (osl--draw)) =0C ;;; Demo =0C ;;;; org-capture =0C (require 'org) (require 'org-capture) (defvar org-capture--current-goto nil) (defvar org-capture--current-keys nil) (defvar org-capture--old-window-config nil) (defun org-capture-test (&optional goto keys) "Simple illustration to recreate org-capture menu (visually only)." (interactive "P") (let ((org-select-vertical-separator "-") (org-capture-templates (or (org-contextualize-keys (org-capture-upgrade-templates org-capture-templates) org-capture-templates-contexts) '(("t" "Task" entry (file+headline "" "Tasks") "* TODO %?\n %u\n %a"))))) (if keys (or (assoc keys org-capture-templates) (error "No capture template referred to by \"%s\" keys" keys))) (cond ((equal goto '(4)) (org-capture-goto-target keys)) ((equal goto '(16)) (org-capture-goto-last-stored)) (t (if goto (setq org-capture--current-goto goto)) (push :org-select-handler org-capture-templates) (push #'org-capture--handler org-capture-templates) (org-select ;; tables `(,(nreverse org-capture-templates) (("C" "Customize org-capture-templates" (customize-variable 'org-capture-templates)) ("q" "Abort" (org-select-quit "Abort")))) ;; description :buffer-name "*Capture*" :key-decorator "[]" :text "Select a capture template\n=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D")))) (message "Org Capture")) ;;(define-key global-map (kbd "C-v c") #'org-capture-test) (defun org-capture--handler (entry org-select-buf) (org-select-quit "" org-select-buf) (let* ((capture-buf (generate-new-buffer "*Capture*")) (annotation (if (and (boundp 'org-capture-link-is-already-stored) org-capture-link-is-already-stored) (plist-get org-store-link-plist :annotation) (ignore-errors (org-store-link nil)))) (entry (or org-capture-entry entry)) (goto org-capture--current-goto) (inhibit-read-only t) initial) (setq initial (or org-capture-initial (and (org-region-active-p) (buffer-substring (point) (mark))))) (when (stringp initial) (remove-text-properties 0 (length initial) '(read-only t) initial)) (when (stringp annotation) (remove-text-properties 0 (length annotation) '(read-only t) annotation)) (org-capture-set-plist entry) (org-capture-get-template) (org-capture-put :original-buffer capture-buf :original-file (or (buffer-file-name capture-buf) (and (featurep 'dired) (car (rassq capture-buf dired-buffers)))) :original-file-nondirectory (and (buffer-file-name capture-buf) (file-name-nondirectory (buffer-file-name capture-buf))) :annotation annotation :initial initial :return-to-wconf (current-window-configuration) :default-time (or org-overriding-default-time (org-current-time))) (org-capture-set-target-location (and (equal goto 0) 'here)) (condition-case error (org-capture-put :template (org-capture-fill-template)) ((error quit) (if (get-buffer capture-buf) (kill-buffer capture-buf)) (error "Capture abort: %s" (error-message-string error)))) (setq org-capture-clock-keep (org-capture-get :clock-keep)) (condition-case error (org-capture-place-template (eq (car (org-capture-get :target)) 'function)) ((error quit) (when (and (buffer-base-buffer (current-buffer)) (string-prefix-p "CAPTURE-" (buffer-name))) (kill-buffer (current-buffer))) (set-window-configuration (org-capture-get :return-to-wconf)) (error "Capture template `%s': %s" (org-capture-get :key) (error-message-string error)))) (when (and (derived-mode-p 'org-mode) (org-capture-get :clock-in)) (condition-case nil (progn (when (org-clock-is-active) (org-capture-put :interrupted-clock (copy-marker org-clock-marker))) (org-clock-in) (setq-local org-capture-clock-was-started t)) (error "Could not start the clock in this capture buffer"))) (when (org-capture-get :immediate-finish) (org-capture-finalize)))) =0C ;;;; Org Agenda =0C (require 'org-agenda) (defvar org-agenda--arg nil) (defvar org-agenda--keys nil) (defvar org-agenda--restriction nil) (defun org-agenda-test (&optional _arg _keys _restriction) (interactive "P") (let ((org-select-horizontal-separator " ")) (org-select '((("a" "Agenda for current week or day" org-agenda-list) ("t" "List of all TODO entries" org-todo-list) ("m" "Match a TAGS/PROP/TODO query" org-tags-view) ("s" "Search for keywords" org-search-view) ("/" "Multi-occur" (call-interactively 'org-occur-in-agenda-files) :org-select-inhibit-transien= t t) ("?" "Find :FLAGGED: entries" (org-tags-view nil "+FLAGGED")) ("*" "Toggle sticky agenda views" org-toggle-sticky-agenda :org-select-handler org-selec= t-run)) (("<" "Buffer, subtree/region restriction" ignore) (">" "Remove restriction" ignore) ("e" "Export agenda views" org-store-agenda-views) ("T" "Entries with special TODO kwd" (org-call-with-arg 'org-todo-list (or org-agenda--arg '(4)))) ("M" "Like m, but only TODO entries" (org-call-with-arg 'org-tags-view (or org-agenda--arg '(4)))) ("S" "Like s, but only TODO entries" (org-call-with-arg 'org-search-view (or org-agenda--arg '(4)))) ("C" "Configure custom agenda commands" (customize-variable 'org-agenda-custom-commands)) ("#" "List stuck projects" (org-agenda--exec 'org-agenda-list-stuck-projects)) ("!" "Configure stuck projects" (customize-variable 'org-stuck-projects)))) :text "Press key for an agenda command: --------------------------------\n" :horizontal t :buffer-name "*Agenda Commands*"))) =0C ;;;; Various tests =0C (defun test1 () "Stays after a choice is made." (interactive) (let ((org-select-horizontal-separator "=E2=94=82")) (org-select ;; table '((("1" "One" (message "One!")) ("2" "Two" (message "Two!!")) ("3" "Three" (message "Three!!!"))) (("C-4" "Four" (message "Four!!!!")) ("C-5" "Five" (message "Five!!!!!")) ("C-6" "six" (message "Six!"))) (("M-7" "Seven" (message "Seven!")) ("M-8" "Eight" (message "Eight!")) ("M-9" "Nine" (message "Nine!")))) ;; description :horizontal t :key-decorator "<>"))) (defun test2 () "Dissapears after a choice is made." (interactive) (let ((org-select-horizontal-separator "=E2=94=82")) (org-select ;; menus '((("h" "Hello, World!" (message "Hello, World!")) ("b" "Bar" (message "Hello, Bar!"))) (("f" "Find File" find-file) ("o" "Open File" (flet ((next-read-file-uses-dialog-p () t)) (call-interactively #'find-file))))) ;; description :key-decorator "\"\"" :transient t) ;; Hints (setq header-line-format (if (not (pos-visible-in-window-p (point-max))) "Use C-v, M-v, C-n or C-p to navigate. C-g, q to quit." "Use C-p/Left to go back, C-g, q to quit.")))) (defun test3 () "Illustrate nested menus, unicode separator and alternative decorator." (interactive) (let ((org-select-vertical-separator "=E2=94=80")) (org-select ;; tables '((("g" "Greetings") ("gh" "Hello, World!" (message "Hello, World!")) ("gb" "Bar" (message "Hello, Bar!"))) (("f" "Functions") ("ff" "Find File" find-file) ("fo" "Open File" (flet ((next-read-file-uses-dialog-p () t)) (call-interactively #'find-file))))))) ;; Hints (setq header-line-format (if (not (pos-visible-in-window-p (point-max))) "Use C-v, M-v, C-n or C-p to navigate. C-g, q to quit." "Use C-p/Left to go back, C-g, q to quit."))) (provide 'org-select) ;;; org-select.el ends here --=-=-=--